<commit_before>/*
 * Copyright 2011 Software Freedom Conservancy
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
import sendkeysMap from "./sendkeysMap";
import { Utils } from "../common/utils";
import storage from "../common/storage";
import { browser } from "webextension-polyfill-ts";

export class Sideex {
    constructor(browserBot) {
        this.browserBot = browserBot;
        this.window = this.browserBot.window;
        this.mouseSpeed = 10;
        this.mouseOverElement = null;
        this.waitForStartTime = 0;
        this.div = "";
        this.wait = {};
        this.window.addEventListener("message", (event) => {
            if (event.source == window && event.data) {
                if (event.data.direction == "from-page-playback-auto-wait") {
                    this.wait.type = event.data.type;
                    this.wait.result = event.data.result;
                }
            }
        });
    }
    hasCommand(command) {
        return Sideex.commands[command] != null;
    }
    async doCommand(command, target, value) {
        return await Sideex.commands[command].call(this, target, value);
    }
    async doAutoWait(type, value) {
        console.log(type);
        console.log(value);
        this.window.postMessage({
            direction: "from-content-playback-auto-wait",
            type: type,
            preWaitTime: value
        }, "*");
        return await this.getWaitResult();
    }
    async getWaitResult() {
        while (Object.keys(this.wait).length == 0) {
            await Utils.delay(10);
        }
        if (this.wait.result) {
            this.wait = {};
            return;
        } else {
            let errorMessage = "";
            switch (this.wait.type) {
                case "pageWait":
                    errorMessage = "Page Wait timed out";
                    break;
                case "ajaxWait":
                    errorMessage = "Ajax Wait timed out";
                    break;
                case "resourceWait":
                    errorMessage = "Resource Wait timed out";
                    break;
            }
            this.wait = {};
            throw errorMessage;
        }
    }

    async doVerifyLocator(commandInfo) {
        let excludedCommand = ["open", "selectWindow", "selectFrame", "close", "assertAlert", "assertConfirmation", "chooseOkOnNextConfirmation",
            "chooseCancelOnNextConfirmation", "assertPrompt", "answerOnNextPrompt", "chooseCancelOnNextPrompt", "echo", "runScript",
            "pause", "store", "storeEval", "storeTitle", "verifyTitle", "assertTitle"];
        if (excludedCommand.includes(commandInfo.command)) {
            return commandInfo.target;
        }
        try {
            let element = this.browserBot.findElement(commandInfo.target);
            return Utils.xpathGenerator(element);
        } catch (e) {
            return commandInfo.target;
        }
    }

    getClientXY(element, coordString) {
        // Parse coordString
        let x = 0;
        let y = 0;
        if (coordString) {
            const coords = coordString.split(/,/);
            x = +coords[0];
            y = +coords[1];
        }
        const r = element.getBoundingClientRect();
        // Get position of element,
        // Return 2 item array with clientX and clientY
        //return [Selenium.prototype.getElementPositionLeft(element) + x, Selenium.prototype.getElementPositionTop(element) + y];
        return [Math.floor(r.left) + x, Math.floor(r.top) + y];
    }

    /** Gets the title of the current page.
     *
     * @return string the title of the current page
     */
    getTitle() {
        return this.browserBot.getTitle();
    }
    /**
     * Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter).
     * For checkbox/radio elements, the value will be "on" or "off" depending on
     * whether the element is checked or not.
     *
     * @param locator an element locator
     * @return string the element value, or "on/off" for checkbox/radio elements
     */
    getValue(locator) {
        var element = this.browserBot.findElement(locator);
        return Utils.getInputValue(element).trim();
    }
    /** Gets the result of evaluating the specified JavaScript snippet.  The snippet may
     * have multiple lines, but only the result of the last line will be returned.
     *
     *
Note that, by default, the snippet will run in the context of the "selenium"
     * object itself, so this will refer to the Selenium object.  Use window to
     * refer to the window of your application, e.g. window.document.getElementById('foo')

     *
     *
If you need to use
     * a locator to refer to a single element in your application page, you can
     * use this.browserBot.findElement("id=foo") where "id=foo" is your locator.

     *
     * @param script the JavaScript snippet to run
     * @return string the results of evaluating the snippet
     */
    getEval(script) {
        try {
            //LOG.info('script is: ' + script);
            var window = this.browserBot.getCurrentWindow();
            var result = eval(script);
            // Selenium RC doesn't allow returning null
            if (null == result)
                return "null";
            return result;
        } catch (e) {
            throw new Utils.SeleniumError("Threw an exception: " + Utils.extractExceptionMessage(e));
        }
    }
    /**
     * Retrieves the horizontal position of an element
     *
     * @param locator an element locator pointing to an element OR an element itself
     * @return number of pixels from the edge of the frame.
     */
    getElementPositionLeft(locator) {
        var element;
        if ("string" == typeof locator) {
            element = this.browserBot.findElement(locator);
        } else {
            element = locator;
        }
        var x = element.offsetLeft;
        var elementParent = element.offsetParent;
        while (elementParent != null) {
            if (document.all) {
                if ((elementParent.tagName != "TABLE") && (elementParent.tagName != "BODY")) {
                    x += elementParent.clientLeft;
                }
            } else { // Netscape/DOM
                if (elementParent.tagName == "TABLE") {
                    var parentBorder = parseInt(elementParent.border);
                    if (isNaN(parentBorder)) {
                        var parentFrame = elementParent.getAttribute('frame');
                        if (parentFrame != null) {
                            x += 1;
                        }
                    } else if (parentBorder > 0) {
                        x += parentBorder;
                    }
                }
            }
            x += elementParent.offsetLeft;
            elementParent = elementParent.offsetParent;
        }
        return x;
    }
    /**
     * Retrieves the vertical position of an element
     *
     * @param locator an element locator pointing to an element OR an element itself
     * @return number of pixels from the edge of the frame.
     */
    getElementPositionTop(locator) {
        var element;
        if ("string" == typeof locator) {
            element = this.browserBot.findElement(locator);
        } else {
            element = locator;
        }
        var y = 0;
        while (element != null) {
            if (document.all) {
                if ((element.tagName != "TABLE") && (element.tagName != "BODY")) {
                    y += element.clientTop;
                }
            } else { // Netscape/DOM
                if (element.tagName == "TABLE") {
                    var parentBorder = parseInt(element.border);
                    if (isNaN(parentBorder)) {
                        var parentFrame = element.getAttribute('frame');
                        if (parentFrame != null) {
                            y += 1;
                        }
                    } else if (parentBorder > 0) {
                        y += parentBorder;
                    }
                }
            }
            y += element.offsetTop;
            // Netscape can get confused in some cases, such that the height of the parent is smaller
            // than that of the element (which it shouldn't really be). If this is the case, we need to
            // exclude this element, since it will result in too large a 'top' return value.
            if (element.offsetParent && element.offsetParent.offsetHeight && element.offsetParent.offsetHeight < element.offsetHeight) {
                // skip the parent that's too small
                element = element.offsetParent.offsetParent;
            } else {
                // Next up...
                element = element.offsetParent;
            }
        }
        return y;
    }
    /**
     * Retrieves the width of an element
     *
     * @param locator an element locator pointing to an element
     * @return number width of an element in pixels
     */
    getElementWidth(locator) {
        var element = this.browserBot.findElement(locator);
        return element.offsetWidth;
    }
    /**
     * Retrieves the height of an element
     *
     * @param locator an element locator pointing to an element
     * @return number height of an element in pixels
     */
    getElementHeight(locator) {
        var element = this.browserBot.findElement(locator);
        return element.offsetHeight;
    }
}

Sideex.commands = {
    /**
     * Clicks on a link, button, checkbox or radio button. If the click action
     * causes a new page to load (like a link usually does), call
     * waitForPageToLoad.
     *
     * @param locator an element locator
     * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse
     *      event relative to the element returned by the locator.
     *
     */
    async clickAt(locator, coordString) {
        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.triggerFocusEvent(element);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        // END
    },
     /**
         * Double clicks on a link, button, checkbox or radio button. If the double click action
         * causes a new page to load (like a link usually does), call
         * waitForPageToLoad.
         *
         * @param locator an element locator
         *
         */
    async doubleClickAt(locator,coordString) {

        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.triggerFocusEvent(element);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'dblclick', true, clientXY[0], clientXY[1]);
        // END

    },
    async mouseOver(locator, coordString) {
        /**
         * Simulates a user hovering a mouse over the specified element.
         *
         * @param locator an element locator
         */
        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
    },
    /**
     * Opens an URL in the test frame. This accepts both relative and absolute
     * URLs.
     *
     * The "open" command waits for the page to load before proceeding,
     * ie. the "AndWait" suffix is implicit.
     *
     * Note: The URL must be on the same domain as the runner HTML
     * due to security restrictions in the browser (Same Origin Policy). If you
     * need to open an URL on another domain, use the Selenium Server to start a
     * new browser session on that domain.
     *
     * @param url the URL to open; may be relative or absolute
     * @param ignoreResponseCode (optional) turn off ajax head request functionality
     *
     */
    async open(url, ignoreResponseCode) {
        if (ignoreResponseCode == null || ignoreResponseCode.length == 0) {
            this.browserBot.ignoreResponseCode = true;
        } else if (ignoreResponseCode.toLowerCase() == "true") {
            this.browserBot.ignoreResponseCode = true;
        } else {
            this.browserBot.ignoreResponseCode = false;
        }
        this.browserBot.openLocation(url);
        window.scrollTo(0, 0);
    }
};<commit_msg>play Drag And Drop<commit_after>/*
 * Copyright 2011 Software Freedom Conservancy
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
import sendkeysMap from "./sendkeysMap";
import { Utils } from "../common/utils";
import storage from "../common/storage";
import { browser } from "webextension-polyfill-ts";

export class Sideex {
    constructor(browserBot) {
        this.browserBot = browserBot;
        this.window = this.browserBot.window;
        this.mouseSpeed = 10;
        this.mouseOverElement = null;
        this.waitForStartTime = 0;
        this.div = "";
        this.wait = {};
        this.window.addEventListener("message", (event) => {
            if (event.source == window && event.data) {
                if (event.data.direction == "from-page-playback-auto-wait") {
                    this.wait.type = event.data.type;
                    this.wait.result = event.data.result;
                }
            }
        });
    }
    hasCommand(command) {
        return Sideex.commands[command] != null;
    }
    async doCommand(command, target, value) {
        return await Sideex.commands[command].call(this, target, value);
    }
    async doAutoWait(type, value) {
        console.log(type);
        console.log(value);
        this.window.postMessage({
            direction: "from-content-playback-auto-wait",
            type: type,
            preWaitTime: value
        }, "*");
        return await this.getWaitResult();
    }
    async getWaitResult() {
        while (Object.keys(this.wait).length == 0) {
            await Utils.delay(10);
        }
        if (this.wait.result) {
            this.wait = {};
            return;
        } else {
            let errorMessage = "";
            switch (this.wait.type) {
                case "pageWait":
                    errorMessage = "Page Wait timed out";
                    break;
                case "ajaxWait":
                    errorMessage = "Ajax Wait timed out";
                    break;
                case "resourceWait":
                    errorMessage = "Resource Wait timed out";
                    break;
            }
            this.wait = {};
            throw errorMessage;
        }
    }

    async doVerifyLocator(commandInfo) {
        let excludedCommand = ["open", "selectWindow", "selectFrame", "close", "assertAlert", "assertConfirmation", "chooseOkOnNextConfirmation",
            "chooseCancelOnNextConfirmation", "assertPrompt", "answerOnNextPrompt", "chooseCancelOnNextPrompt", "echo", "runScript",
            "pause", "store", "storeEval", "storeTitle", "verifyTitle", "assertTitle"];
        if (excludedCommand.includes(commandInfo.command)) {
            return commandInfo.target;
        }
        try {
            let element = this.browserBot.findElement(commandInfo.target);
            return Utils.xpathGenerator(element);
        } catch (e) {
            return commandInfo.target;
        }
    }

    getClientXY(element, coordString) {
        // Parse coordString
        let x = 0;
        let y = 0;
        if (coordString) {
            const coords = coordString.split(/,/);
            x = +coords[0];
            y = +coords[1];
        }
        const r = element.getBoundingClientRect();
        // Get position of element,
        // Return 2 item array with clientX and clientY
        //return [Selenium.prototype.getElementPositionLeft(element) + x, Selenium.prototype.getElementPositionTop(element) + y];
        return [Math.floor(r.left) + x, Math.floor(r.top) + y];
    }

    /** Gets the title of the current page.
     *
     * @return string the title of the current page
     */
    getTitle() {
        return this.browserBot.getTitle();
    }
    /**
     * Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter).
     * For checkbox/radio elements, the value will be "on" or "off" depending on
     * whether the element is checked or not.
     *
     * @param locator an element locator
     * @return string the element value, or "on/off" for checkbox/radio elements
     */
    getValue(locator) {
        var element = this.browserBot.findElement(locator);
        return Utils.getInputValue(element).trim();
    }
    /** Gets the result of evaluating the specified JavaScript snippet.  The snippet may
     * have multiple lines, but only the result of the last line will be returned.
     *
     *
Note that, by default, the snippet will run in the context of the "selenium"
     * object itself, so this will refer to the Selenium object.  Use window to
     * refer to the window of your application, e.g. window.document.getElementById('foo')

     *
     *
If you need to use
     * a locator to refer to a single element in your application page, you can
     * use this.browserBot.findElement("id=foo") where "id=foo" is your locator.

     *
     * @param script the JavaScript snippet to run
     * @return string the results of evaluating the snippet
     */
    getEval(script) {
        try {
            //LOG.info('script is: ' + script);
            var window = this.browserBot.getCurrentWindow();
            var result = eval(script);
            // Selenium RC doesn't allow returning null
            if (null == result)
                return "null";
            return result;
        } catch (e) {
            throw new Utils.SeleniumError("Threw an exception: " + Utils.extractExceptionMessage(e));
        }
    }
    /**
     * Retrieves the horizontal position of an element
     *
     * @param locator an element locator pointing to an element OR an element itself
     * @return number of pixels from the edge of the frame.
     */
    getElementPositionLeft(locator) {
        var element;
        if ("string" == typeof locator) {
            element = this.browserBot.findElement(locator);
        } else {
            element = locator;
        }
        var x = element.offsetLeft;
        var elementParent = element.offsetParent;
        while (elementParent != null) {
            if (document.all) {
                if ((elementParent.tagName != "TABLE") && (elementParent.tagName != "BODY")) {
                    x += elementParent.clientLeft;
                }
            } else { // Netscape/DOM
                if (elementParent.tagName == "TABLE") {
                    var parentBorder = parseInt(elementParent.border);
                    if (isNaN(parentBorder)) {
                        var parentFrame = elementParent.getAttribute('frame');
                        if (parentFrame != null) {
                            x += 1;
                        }
                    } else if (parentBorder > 0) {
                        x += parentBorder;
                    }
                }
            }
            x += elementParent.offsetLeft;
            elementParent = elementParent.offsetParent;
        }
        return x;
    }
    /**
     * Retrieves the vertical position of an element
     *
     * @param locator an element locator pointing to an element OR an element itself
     * @return number of pixels from the edge of the frame.
     */
    getElementPositionTop(locator) {
        var element;
        if ("string" == typeof locator) {
            element = this.browserBot.findElement(locator);
        } else {
            element = locator;
        }
        var y = 0;
        while (element != null) {
            if (document.all) {
                if ((element.tagName != "TABLE") && (element.tagName != "BODY")) {
                    y += element.clientTop;
                }
            } else { // Netscape/DOM
                if (element.tagName == "TABLE") {
                    var parentBorder = parseInt(element.border);
                    if (isNaN(parentBorder)) {
                        var parentFrame = element.getAttribute('frame');
                        if (parentFrame != null) {
                            y += 1;
                        }
                    } else if (parentBorder > 0) {
                        y += parentBorder;
                    }
                }
            }
            y += element.offsetTop;
            // Netscape can get confused in some cases, such that the height of the parent is smaller
            // than that of the element (which it shouldn't really be). If this is the case, we need to
            // exclude this element, since it will result in too large a 'top' return value.
            if (element.offsetParent && element.offsetParent.offsetHeight && element.offsetParent.offsetHeight < element.offsetHeight) {
                // skip the parent that's too small
                element = element.offsetParent.offsetParent;
            } else {
                // Next up...
                element = element.offsetParent;
            }
        }
        return y;
    }
    /**
     * Retrieves the width of an element
     *
     * @param locator an element locator pointing to an element
     * @return number width of an element in pixels
     */
    getElementWidth(locator) {
        var element = this.browserBot.findElement(locator);
        return element.offsetWidth;
    }
    /**
     * Retrieves the height of an element
     *
     * @param locator an element locator pointing to an element
     * @return number height of an element in pixels
     */
    getElementHeight(locator) {
        var element = this.browserBot.findElement(locator);
        return element.offsetHeight;
    }
}

Sideex.commands = {
    /**
     * Clicks on a link, button, checkbox or radio button. If the click action
     * causes a new page to load (like a link usually does), call
     * waitForPageToLoad.
     *
     * @param locator an element locator
     * @param coordString specifies the x,y position (i.e. - 10,20) of the mouse
     *      event relative to the element returned by the locator.
     *
     */
    async clickAt(locator, coordString) {
        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.triggerFocusEvent(element);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        // END
    },
     /**
         * Double clicks on a link, button, checkbox or radio button. If the double click action
         * causes a new page to load (like a link usually does), call
         * waitForPageToLoad.
         *
         * @param locator an element locator
         *
         */
    async doubleClickAt(locator,coordString) {

        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.triggerFocusEvent(element);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mousedown', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'mouseup', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'click', true, clientXY[0], clientXY[1]);
        this.browserBot.fireMouseEvent(element, 'dblclick', true, clientXY[0], clientXY[1]);
        // END

    },
    async mouseOver(locator, coordString) {
        /**
         * Simulates a user hovering a mouse over the specified element.
         *
         * @param locator an element locator
         */
        var element = this.browserBot.findElement(locator);
        var clientXY = this.getClientXY(element, coordString);
        this.browserBot.fireMouseEvent(element, 'mouseover', true, clientXY[0], clientXY[1]);
    },
    async dragAndDrop(locator, movementsString, coordString) {
        /** Drags an element a certain distance and then drops it
         * @param locator an element locator
         * @param movementsString offset in pixels from the current location to which the element should be moved, e.g., "+70,-300"
         */
        var element = this.browserBot.findElement(locator, coordString);
        var clientStartXY = getClientXY(element)
        var clientStartX = clientStartXY[0];
        var clientStartY = clientStartXY[1];

        var movements = movementsString.split(/,/);
        var movementX = Number(movements[0]);
        var movementY = Number(movements[1]);

        var clientFinishX = ((clientStartX + movementX) < 0) ? 0 : (clientStartX + movementX);
        var clientFinishY = ((clientStartY + movementY) < 0) ? 0 : (clientStartY + movementY);

        var mouseSpeed = this.mouseSpeed;
        var move = function(current, dest) {
            if (current == dest) return current;
            if (Math.abs(current - dest) < mouseSpeed) return dest;
            return (current < dest) ? current + mouseSpeed : current - mouseSpeed;
        }

        this.browserBot.triggerMouseEvent(element, 'mousedown', true, clientStartX, clientStartY);
        this.browserBot.triggerMouseEvent(element, 'mousemove', true, clientStartX, clientStartY);
        var clientX = clientStartX;
        var clientY = clientStartY;

        while ((clientX != clientFinishX) || (clientY != clientFinishY)) {
            clientX = move(clientX, clientFinishX);
            clientY = move(clientY, clientFinishY);
            this.browserbot.triggerMouseEvent(element, 'mousemove', true, clientX, clientY);
        }

        this.browserBot.triggerMouseEvent(element, 'mousemove', true, clientFinishX, clientFinishY);
        this.browserBot.triggerMouseEvent(element, 'mouseup', true, clientFinishX, clientFinishY);
    },

    async dragAndDropToObject(locatorOfObjectToBeDragged, locatorOfDragDestinationObject) {
        /** Drags an element and drops it on another element
         *
         * @param locatorOfObjectToBeDragged an element to be dragged
         * @param locatorOfDragDestinationObject an element whose location (i.e., whose center-most pixel) will be the point where locatorOfObjectToBeDragged  is dropped
         */
        if (!this.browserBot.findElement(locatorOfObjectToBeDragged).draggable) {
            //origin code
            var startX = this.getElementPositionLeft(locatorOfObjectToBeDragged);
            var startY = this.getElementPositionTop(locatorOfObjectToBeDragged);

            var destinationLeftX = this.getElementPositionLeft(locatorOfDragDestinationObject);
            var destinationTopY = this.getElementPositionTop(locatorOfDragDestinationObject);
            var destinationWidth = this.getElementWidth(locatorOfDragDestinationObject);
            var destinationHeight = this.getElementHeight(locatorOfDragDestinationObject);

            var endX = Math.round(destinationLeftX + (destinationWidth / 2));
            var endY = Math.round(destinationTopY + (destinationHeight / 2));

            var deltaX = endX - startX;
            var deltaY = endY - startY;

            var movementsString = "" + deltaX + "," + deltaY;
            this.doDragAndDrop(locatorOfObjectToBeDragged, movementsString);
        } else {
            // © Shuo-Heng Shih, SideeX Team
            var element = this.browserBot.findElement(locatorOfObjectToBeDragged);
            var target = this.browserBot.findElement(locatorOfDragDestinationObject);
            this.browserBot.triggerDragEvent(element, target);
            // END
        }
    },
    /**
     * Opens an URL in the test frame. This accepts both relative and absolute
     * URLs.
     *
     * The "open" command waits for the page to load before proceeding,
     * ie. the "AndWait" suffix is implicit.
     *
     * Note: The URL must be on the same domain as the runner HTML
     * due to security restrictions in the browser (Same Origin Policy). If you
     * need to open an URL on another domain, use the Selenium Server to start a
     * new browser session on that domain.
     *
     * @param url the URL to open; may be relative or absolute
     * @param ignoreResponseCode (optional) turn off ajax head request functionality
     *
     */
    async open(url, ignoreResponseCode) {
        if (ignoreResponseCode == null || ignoreResponseCode.length == 0) {
            this.browserBot.ignoreResponseCode = true;
        } else if (ignoreResponseCode.toLowerCase() == "true") {
            this.browserBot.ignoreResponseCode = true;
        } else {
            this.browserBot.ignoreResponseCode = false;
        }
        this.browserBot.openLocation(url);
        window.scrollTo(0, 0);
    }
};<|endoftext|>


<commit_before>import EventEmitter from 'events';
import get from 'lodash-es/get';
import has from 'lodash-es/has';
import isEqual from 'lodash-es/isEqual';
import merge from 'lodash-es/merge';
import set from 'lodash-es/set';
import unset from 'lodash-es/unset';
import odiff from 'odiff';
import pathToRegexp from 'path-to-regexp';
import { ScolaError } from '@scola/error';

export default class Observable extends EventEmitter {
  constructor() {
    super();

    this._path = null;
    this._parser = null;
    this._keys = [];

    this._connection = null;

    this._auth = false;
    this._mode = 'object';
    this._state = 'idle';

    this._local = {};
    this._remote = {};

    this._etag = null;
    this._total = 0;

    this._subscribed = false;
    this._selected = false;

    this._request = null;
    this._response = null;

    this._handleAuth = (v) => this._open(v);
    this._handleData = (d) => this._data(d);
    this._handleEnd = () => this._destroy();
    this._handleError = (e) => this._select(e);
    this._handleOpen = () => this._open();
    this._handleResponse = (r) => this._select(null, r);
  }

  destroy() {
    this._unbindConnection();
    this._destroy();

    this._connection = null;
    this._local = {};
    this._remote = {};
  }

  path(value = null) {
    if (value === null) {
      return this._path;
    }

    if (value === true) {
      return this._parser(this._local);
    }

    this._path = value;
    this._parser = pathToRegexp.compile(value);

    pathToRegexp(value, this._keys);

    return this;
  }

  connection(value = null) {
    if (value === null) {
      return this._connection;
    }

    this._connection = value;
    this._bindConnection();

    return this;
  }

  auth(value = null) {
    if (value === null) {
      return this._auth;
    }

    this._auth = value;
    return this;
  }

  mode(value = null) {
    if (value === null) {
      return this._mode;
    }

    this._mode = value;
    this._remote = value === 'list' ? [] : {};

    return this;
  }

  local(value = null) {
    if (value === null) {
      return this._local;
    }

    this._local = {};
    this.assign(value);

    return this;
  }

  remote(value = null) {
    if (value === null) {
      return this._remote;
    }

    this._remote = value;

    if (this._mode === 'object') {
      this.local(value);
    }

    return this;
  }

  etag(value = null) {
    if (value === null) {
      return this._etag;
    }

    this._etag = value;
    return this;
  }

  total(value = null) {
    if (value === null) {
      return this._total;
    }

    this._total = value;
    return this;
  }

  connected() {
    return this._connection.writable();
  }

  diff() {
    return odiff(this._remote, this._local);
  }

  state() {
    return this._state;
  }

  get(name) {
    return get(this._local, name);
  }

  has(name) {
    return has(this._local, name);
  }

  set(name, value, scope) {
    const old = this.get(name);
    const changed = !isEqual(old, value);

    set(this._local, name, value);

    this.emit('set', {
      changed,
      name,
      value,
      scope
    });

    return this;
  }

  unset(name) {
    unset(this._local, name);
    return;
  }

  add(name, value, action = true) {
    const values = this.get(name) || [];

    if (action === true) {
      values.push(value);
    } else if (action === false) {
      values.splice(values.indexOf(value), 1);
    }

    return this.set(name, values.sort());
  }

  assign(values, scope) {
    Object.keys(values).forEach((key) => {
      this.set(key, values[key], scope);
    });

    return this;
  }

  flush() {
    this._local = {};
    this._remote = {};

    return this;
  }

  subscribe(action = null) {
    if (action === null) {
      return this._subscribed;
    }

    this._subscribed = action;

    if (action === false) {
      this._unsubscribe();
    }

    return this;
  }

  select() {
    this._selected = true;

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    if (this._auth === true && !this._connection.auth()) {
      this.emit('error', new ScolaError('401 invalid_user'));
      return this;
    }

    if (this._subscribed === true) {
      this._unsubscribe(false);
    }

    const [path, local] = this._parse();

    this._request = this._connection
      .request()
      .method('GET')
      .path(path);

    if (this._mode === 'list') {
      this._request.query(local);
    }

    if (this._etag) {
      this._request.header('x-etag', this._etag);
    }

    this._bindRequest();

    if (this._subscribed === true) {
      this._request.write('');
    } else {
      this._request.end();
    }

    return this;
  }

  insert() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path, local] = this._parse();

    const request = this._connection
      .request()
      .method('POST')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._insert(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._insert(null, response);
    });

    request.end(local);
    return this;
  }

  update() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path, local] = this._parse();

    const request = this._connection
      .request()
      .method('PUT')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._update(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._update(null, response);
    });

    request.end(local);
    return this;
  }

  delete() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path] = this._parse();

    const request = this._connection
      .request()
      .method('DELETE')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._delete(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._delete(null, response);
    });

    request.end();
    return this;
  }

  _bindConnection() {
    if (!this._connection) {
      return;
    }

    if (this._auth === true) {
      this._connection.on('auth', this._handleAuth);
    } else {
      this._connection.on('open', this._handleOpen);
    }
  }

  _unbindConnection() {
    if (!this._connection) {
      return;
    }

    if (this._auth === true) {
      this._connection.removeListener('auth', this._handleAuth);
    } else {
      this._connection.removeListener('open', this._handleOpen);
    }
  }

  _bindRequest() {
    if (this._request) {
      this._request.on('error', this._handleError);
      this._request.on('response', this._handleResponse);
    }
  }

  _unbindRequest() {
    if (this._request) {
      this._request.removeListener('error', this._handleError);
      this._request.removeListener('response', this._handleResponse);
    }
  }

  _bindResponse() {
    if (this._response) {
      this._response.on('data', this._handleData);
      this._response.on('end', this._handleEnd);
      this._response.on('error', this._handleError);
    }
  }

  _unbindResponse() {
    if (this._response) {
      this._response.removeListener('data', this._handleData);
      this._response.removeListener('end', this._handleEnd);
      this._response.removeListener('error', this._handleError);
    }
  }

  _unsubscribe(properties = true) {
    if (this._request) {
      this._request.header('x-etag', false);
      this._request.end();
    }

    if (properties === true) {
      this._etag = null;
      this._total = null;
    }

    this._destroy();
  }

  _select(error, response) {
    if (error) {
      this._error(error);
      return;
    }

    this._response = response;
    this._bindResponse();
  }

  _insert(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 201) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.emit('insert', data);
    });
  }

  _update(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 200) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.remote(data);
      this.emit('update', this._remote);
    });
  }

  _delete(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 200) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.flush(true);
      this.emit('delete');
    });
  }

  _data(data) {
    if (!this._response) {
      return;
    }

    if (this._response.status() >= 400) {
      this._total = 0;
      this._error(new ScolaError(data));
      return;
    }

    if (this._response.header('x-publish') === 1) {
      this.emit('publish', data);
      return;
    }

    if (this._response.header('x-etag')) {
      this._etag = this._response.header('x-etag');
    }

    if (this._response.header('x-total')) {
      this._total = Number(this._response.header('x-total'));
    }

    if (this._response.status() === 200) {
      this.remote(data);
    }

    this.emit('select', this._remote, this._total, this._etag);
  }

  _error(error) {
    this._state = 'idle';
    this._destroy();

    this.emit('error', error);
  }

  _destroy() {
    if (this._request) {
      this._unbindRequest();
      this._request.destroy();
      this._request = null;
    }

    if (this._response) {
      this._unbindResponse();
      this._response.destroy();
      this._response = null;
    }
  }

  _extract(response, callback) {
    const chunks = [];

    response.once('error', (error) => {
      response.removeAllListeners();
      this.emit('error', error);
    });

    response.on('data', (chunk) => {
      chunks.push(chunk);
    });

    response.once('end', () => {
      response.removeAllListeners();

      let data = null;

      if (chunks.length === 1) {
        data = chunks[0];
      } else if (Buffer.isBuffer(chunks[0])) {
        data = Buffer.concat(chunks);
      } else {
        data = chunks.join('');
      }

      callback(data);
    });
  }

  _parse() {
    const local = merge({}, this._local);
    const path = this.path(true);

    this._keys.forEach((key) => {
      delete local[key.name];
    });

    return [path, local];
  }

  _open(value) {
    const select = value !== false &&
      this._selected === true;

    if (select) {
      this.select();
    }
  }
}
<commit_msg>Do not handle connection events, emit select when setting remote<commit_after>import EventEmitter from 'events';
import get from 'lodash-es/get';
import has from 'lodash-es/has';
import isEqual from 'lodash-es/isEqual';
import merge from 'lodash-es/merge';
import set from 'lodash-es/set';
import unset from 'lodash-es/unset';
import odiff from 'odiff';
import pathToRegexp from 'path-to-regexp';
import { ScolaError } from '@scola/error';

export default class Observable extends EventEmitter {
  constructor() {
    super();

    this._path = null;
    this._parser = null;
    this._keys = [];

    this._connection = null;

    this._mode = 'object';
    this._state = 'idle';

    this._local = {};
    this._remote = {};

    this._etag = null;
    this._total = 0;

    this._subscribed = false;

    this._request = null;
    this._response = null;

    this._handleData = (d) => this._data(d);
    this._handleEnd = () => this._destroy();
    this._handleError = (e) => this._select(e);
    this._handleResponse = (r) => this._select(null, r);
  }

  destroy() {
    this._destroy();

    this._connection = null;
    this._local = {};
    this._remote = {};
  }

  path(value = null) {
    if (value === null) {
      return this._path;
    }

    if (value === true) {
      return this._parser(this._local);
    }

    this._path = value;
    this._parser = pathToRegexp.compile(value);

    pathToRegexp(value, this._keys);

    return this;
  }

  connection(value = null) {
    if (value === null) {
      return this._connection;
    }

    this._connection = value;
    return this;
  }

  mode(value = null) {
    if (value === null) {
      return this._mode;
    }

    this._mode = value;
    this._remote = value === 'list' ? [] : {};

    return this;
  }

  local(value = null) {
    if (value === null) {
      return this._local;
    }

    this._local = {};
    this.assign(value);

    return this;
  }

  remote(value = null) {
    if (value === null) {
      return this._remote;
    }

    this._remote = value;

    if (this._mode === 'object') {
      this.local(value);
    }

    this.emit('select', this._remote, this._total, this._etag);
    return this;
  }

  etag(value = null) {
    if (value === null) {
      return this._etag;
    }

    this._etag = value;
    return this;
  }

  total(value = null) {
    if (value === null) {
      return this._total;
    }

    this._total = value;
    return this;
  }

  connected() {
    return this._connection.writable();
  }

  diff() {
    return odiff(this._remote, this._local);
  }

  state() {
    return this._state;
  }

  get(name) {
    return get(this._local, name);
  }

  has(name) {
    return has(this._local, name);
  }

  set(name, value, scope) {
    const old = this.get(name);
    const changed = !isEqual(old, value);

    set(this._local, name, value);

    this.emit('set', {
      changed,
      name,
      value,
      scope
    });

    return this;
  }

  unset(name) {
    unset(this._local, name);
    return;
  }

  add(name, value, action = true) {
    const values = this.get(name) || [];

    if (action === true) {
      values.push(value);
    } else if (action === false) {
      values.splice(values.indexOf(value), 1);
    }

    return this.set(name, values.sort());
  }

  assign(values, scope) {
    Object.keys(values).forEach((key) => {
      this.set(key, values[key], scope);
    });

    return this;
  }

  flush() {
    this._local = {};
    this._remote = {};

    return this;
  }

  subscribe(action = null) {
    if (action === null) {
      return this._subscribed;
    }

    this._subscribed = action;

    if (action === false) {
      this._unsubscribe();
    }

    return this;
  }

  select() {
    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    if (this._subscribed === true) {
      this._unsubscribe(false);
    }

    const [path, local] = this._parse();

    this._request = this._connection
      .request()
      .method('GET')
      .path(path);

    if (this._mode === 'list') {
      this._request.query(local);
    }

    if (this._etag) {
      this._request.header('x-etag', this._etag);
    }

    this._bindRequest();

    if (this._subscribed === true) {
      this._request.write('');
    } else {
      this._request.end();
    }

    return this;
  }

  insert() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path, local] = this._parse();

    const request = this._connection
      .request()
      .method('POST')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._insert(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._insert(null, response);
    });

    request.end(local);
    return this;
  }

  update() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path, local] = this._parse();

    const request = this._connection
      .request()
      .method('PUT')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._update(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._update(null, response);
    });

    request.end(local);
    return this;
  }

  delete() {
    if (this._state === 'busy') {
      this.emit('error', new ScolaError('500 invalid_state'));
      return this;
    }

    if (!this.connected()) {
      this.emit('error', new ScolaError('500 invalid_socket'));
      return this;
    }

    this._state = 'busy';

    const [path] = this._parse();

    const request = this._connection
      .request()
      .method('DELETE')
      .path(path);

    request.once('error', (error) => {
      request.removeAllListeners();
      this._delete(error);
    });

    request.once('response', (response) => {
      request.removeAllListeners();
      this._delete(null, response);
    });

    request.end();
    return this;
  }

  _bindRequest() {
    if (this._request) {
      this._request.on('error', this._handleError);
      this._request.on('response', this._handleResponse);
    }
  }

  _unbindRequest() {
    if (this._request) {
      this._request.removeListener('error', this._handleError);
      this._request.removeListener('response', this._handleResponse);
    }
  }

  _bindResponse() {
    if (this._response) {
      this._response.on('data', this._handleData);
      this._response.on('end', this._handleEnd);
      this._response.on('error', this._handleError);
    }
  }

  _unbindResponse() {
    if (this._response) {
      this._response.removeListener('data', this._handleData);
      this._response.removeListener('end', this._handleEnd);
      this._response.removeListener('error', this._handleError);
    }
  }

  _unsubscribe(properties = true) {
    if (this._request) {
      this._request.header('x-etag', false);
      this._request.end();
    }

    if (properties === true) {
      this._etag = null;
      this._total = null;
    }

    this._destroy();
  }

  _select(error, response) {
    if (error) {
      this.emit('error', error);
      this._destroy();
      return;
    }

    this._response = response;
    this._bindResponse();
  }

  _insert(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 201) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.emit('insert', data);
    });
  }

  _update(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 200) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.emit('update', this._remote);
    });
  }

  _delete(error, response) {
    this._state = 'idle';

    if (error) {
      this.emit('error', error);
      return;
    }

    this._extract(response, (data) => {
      if (response.status() !== 200) {
        this.emit('error', new ScolaError(data));
        return;
      }

      this.flush(true);
      this.emit('delete');
    });
  }

  _data(data) {
    if (!this._response) {
      return;
    }

    if (this._response.status() >= 400) {
      this._error(data);
      return;
    }

    if (this._response.header('x-publish') === 1) {
      this.emit('publish', data);
      return;
    }

    if (this._response.header('x-etag')) {
      this._etag = this._response.header('x-etag');
    }

    if (this._response.header('x-total')) {
      this._total = Number(this._response.header('x-total'));
    }

    if (this._response.status() === 200) {
      this.remote(data);
    }
  }

  _error(data) {
    this.emit('error', new ScolaError(data));
    this._destroy();
    this._total = 0;
  }

  _destroy() {
    if (this._request) {
      this._unbindRequest();
      this._request.destroy();
      this._request = null;
    }

    if (this._response) {
      this._unbindResponse();
      this._response.destroy();
      this._response = null;
    }
  }

  _extract(response, callback) {
    const chunks = [];

    response.once('error', (error) => {
      response.removeAllListeners();
      this.emit('error', error);
    });

    response.on('data', (chunk) => {
      chunks.push(chunk);
    });

    response.once('end', () => {
      response.removeAllListeners();

      let data = null;

      if (chunks.length === 1) {
        data = chunks[0];
      } else if (Buffer.isBuffer(chunks[0])) {
        data = Buffer.concat(chunks);
      } else {
        data = chunks.join('');
      }

      callback(data);
    });
  }

  _parse() {
    const local = merge({}, this._local);
    const path = this.path(true);

    this._keys.forEach((key) => {
      delete local[key.name];
    });

    return [path, local];
  }
}
<|endoftext|>


<commit_before>
(function(global){

  /**
   * Subscribable Objects implement the publish-subscribe pattern.
   * @constructor
   */
  function Subscribable(){

    /**
     * Subscribable can only be called as a constructor.
     */
    if (!this instanceof Subscribable)return new Subscribable();

    /**
     * This gets the subscribers that were registered with this object.
     * The event names server as keys here for values which are arrays
     * containg the subscribers registerd for that event.
     * @type {Object}
     */
    this.getDependencies = function(){
      return this._dependencies || (this._dependencies = {});
    }

    /**
     * The default event indicates that the value has changed.
     * @type {String}
     */
    var defaultEvent = "change";

    /**
     * All subscribers that were registered with the specified event
     * parameter will be called. When a custome notify function is
     * needed hockNotify can be defined to execute object specific logic.
     * @param  {[type]} value - if defined replaced by options.read
     * @param  {[type]} event - when no specified the default is used
     */
    this.notifySubscribers = function(value, event){
      event = event || defaultEvent;
      var dependencies = this.getDependencies()[event] || [];
      try{
        if(this.getter)value = this.getter();
      }finally{
        var length = dependencies.length;
        for(var i = 0; i < length; i++){
          if(!dependencies[i].disposed){
            dependencies[i].cb.call(dependencies[i].context, value);
          }
        }
        for(i = 0; i < length; i++){
          if(dependencies[i].disposed){
            dependencies.splice(i, 1);
            i--; length--;
          }
        }
      }
    };

    /**
     * Add a subscriber to this object. When notify is executed the
     * callback parameter is called with the context specified here.
     * @param  {Function} cb      - the subscription callback
     * @param  {Any}      context - if true replaced with the context that
     *                              subscribe is called with.
     * @param  {String}   event   - when no specified the default is used
     */
    this.subscribe = function(cb, context, event){
      event = event || defaultEvent;
      if(typeof cb !== "function")throw "First parameter musst be a function.";
      var dependency = {cb: cb, context: context};
      (this.getDependencies()[event] || (this.getDependencies()[event] = [])).push(dependency);
      function dispose(){ dependency.disposed = true; };
      return { dispose: dispose };
    };

    /**
     * This copyies all properties from a subscribable object onto another
     * object, which can then be used just like the original subcriber. This
     * is needed so that a function may become a subscriber. In the case of
     * a function this can't be done using just prototype.
     * @param  {Any} target - this target will become subscribable
     */
    this.copyProperties = function(target){
      for(var i in this)target[i] = this[i];
      target._clonedFrom = this;
      return target;
    };

    /**
     * Extend via properties
     * @param  {[type]} properties [description]
     * @return {[type]}            [description]
     */
    this.extend = function(properties){
      for(var i in properties)this[i] = properties[i];
      return this;
    }

    /**
     * Returns the number of subscriptions for the specific event name.
     * @param {String} event - if falsy; all subscriptions are counted
     * @return {Integer} Nr. of subscriptions on this object
     */
    this.getSubscriptionsCount = function(event){
      var count = 0, dependencies = this.getDependencies();
      if(event)return (dependencies[event]||[]).length;
      for(var i in dependencies)count += dependencies[i].length;
      return count;
    }
  }

  var stack = [];
  function recordDependency(dependency){
    var length = stack.length;
    var last = length > 0 ? stack[length-1] : [];
    if(last.observable !== dependency)last.push(dependency);
  }
  function recordExecution(func, context, observable){
    if(!func)return;
    if(observable.recording)return observable.value;
    var result = [];
    result.observable = observable;
    observable.recording = true;
    stack.push(result);
    try{
      return func.apply(context);
    }finally{
      stack.pop();
      observable.recording = false;
      for(var j = 0; j < (observable._subs||[]).length; j++){
        observable._subs[j].dispose();
      }
      observable._subs = [];
      for(var i = 0; i < result.length; i++){
        observable._subs.push(result[i].subscribe(function(){
          observable.peek();
        }));
      }
    }
  }

  function Observable(){

    function self(){
      if(arguments.length > 0) {
        self.setter(arguments[0], true, arguments);
        return this;
      }else{
        return self.getter();
      }
    };

    self.valueHasMutated = function(){
      self.notifySubscribers(self.value);

    };

    self.valueWillMutate = function(){
      self.notifySubscribers(self.value, "beforeChange");
    };

    self.setter = function(value, write, args){
      var compare = self.equalityComparer;
      if(self.notify === 'always' || !compare || !compare(value, self.value)){
        try{
          self.valueWillMutate();
          if(write && !self.write)throw "This observable can't be set.";
          self.value = value;
          if(write)self.write.apply(self.getContext(), args || value);
        }finally{
          self.valueHasMutated();
        }
      }
    };

    self.equalityComparer = function(newvalue, oldvalue){
      var isObject = Object.prototype.toString.call(newvalue) === "[object Object]";
      return !isObject && newvalue === oldvalue;
    };

    self.getter = function(){
      recordDependency(self);
      return self.value;
    };

    self.peek = function(){
      var value = recordExecution(self.read, self.getContext(), self);
      self.setter(value);
      return value;
    };

    self.getContext = function(){
      return self.owner === undefined ? global : self.owner;
    };

    /**
     * Inheritance
     */

    this.copyProperties(self);
    self.original = this;
    return self;
  }

  function DependentObservable(options, owner){

    if(!options || typeof (options.read || options) !== "function"){
      throw "Function has to be passed."
    }

    if (!(this instanceof DependentObservable)){
      return new DependentObservable(options, owner);
    }

    var self = Observable.call(this);

    if(options.read){
      self.extend(options);
    }else{
      self.read = options;
      self.owner = owner;
    }

    if(!self.deferEvaluation)self.peek();

    return self;
  }

  /**
   * Utility function that is used to build type checking functions. The resulting
   * function also checks for necessary properties and also returns true if the
   * copyProperties function was used to instantiate this object.
   *
   * @param  {Constructor} type to be checked against
   * @return {Function}
   */
  function buildCheckType(type, check){
    return function(any){
      return !!any
          && ( any instanceof type || any._clonedFrom instanceof type )
          && (!check || check(any));
    };
  }

  /**
   * Return a result object
   */
  global.ko = {
    subscribable: Subscribable,
    isSubscribable: buildCheckType(Subscribable),
    observable: function(initial){
      var self = new Observable();
      self.value = initial;
      //self.read  = function(){return self.value;};
      self.write = function(){};
      return self;
    },
    dependentObservable: DependentObservable,
    computed: DependentObservable,
    isComputed: buildCheckType(DependentObservable),
    isObservable: buildCheckType(Observable),
    isWriteableObservable: buildCheckType(Observable, function(self){return !!self.write;})
  };

  /**
   * Prototype hirarchy
   */
  Subscribable.fn = Subscribable.prototype = {};
  global.ko.observable.fn = Observable.prototype = new Subscribable();
  DependentObservable.prototype = new Observable().original;

})(window);

      /*defer  = options.deferEvaluation ,
      owner  = options.owner,
      type   = options.type;*/ // computed, array on result

  //result.writable when !!setter

  // dont chnage when newValue === oldValue
  // see notify 'always' propertie on observable
  // add extend shim to qunit.html
  // change for equal object except when equalityComparer === func && return true
  //
  // result.notifySubscribers (can be set) resolved for every call
  //
  //  ko.subscribable.fn or ko.observable.fn musst be inherited
  //
  //  chaning syntax on write calls

  // beforeChange send event

<commit_msg>Computed changes<commit_after>
(function(global){

  /**
   * Subscribable Objects implement the publish-subscribe pattern.
   * @constructor
   */
  function Subscribable(){

    /**
     * Subscribable can only be called as a constructor.
     */
    if (!this instanceof Subscribable)return new Subscribable();

    /**
     * This gets the subscribers that were registered with this object.
     * The event names server as keys here for values which are arrays
     * containg the subscribers registerd for that event.
     * @type {Object}
     */
    this.getDependencies = function(){
      return this._dependencies || (this._dependencies = {});
    }

    /**
     * The default event indicates that the value has changed.
     * @type {String}
     */
    var defaultEvent = "change";

    /**
     * All subscribers that were registered with the specified event
     * parameter will be called. When a custome notify function is
     * needed hockNotify can be defined to execute object specific logic.
     * @param  {[type]} value - if defined replaced by options.read
     * @param  {[type]} event - when no specified the default is used
     */
    this.notifySubscribers = function(value, event){
      event = event || defaultEvent;
      var dependencies = this.getDependencies()[event] || [];
      try{
        if(this.getter)value = this.getter();
      }finally{
        var length = dependencies.length;
        for(var i = 0; i < length; i++){
          if(!dependencies[i].disposed){
            dependencies[i].cb.call(dependencies[i].context, value);
          }
        }
        for(i = 0; i < length; i++){
          if(dependencies[i].disposed){
            dependencies.splice(i, 1);
            i--; length--;
          }
        }
      }
    };

    /**
     * Add a subscriber to this object. When notify is executed the
     * callback parameter is called with the context specified here.
     * @param  {Function} cb      - the subscription callback
     * @param  {Any}      context - if true replaced with the context that
     *                              subscribe is called with.
     * @param  {String}   event   - when no specified the default is used
     */
    this.subscribe = function(cb, context, event){
      event = event || defaultEvent;
      if(typeof cb !== "function")throw "First parameter musst be a function.";
      var dependency = {cb: cb, context: context};
      (this.getDependencies()[event] || (this.getDependencies()[event] = [])).push(dependency);
      function dispose(){ dependency.disposed = true; };
      return { dispose: dispose };
    };

    /**
     * This copyies all properties from a subscribable object onto another
     * object, which can then be used just like the original subcriber. This
     * is needed so that a function may become a subscriber. In the case of
     * a function this can't be done using just prototype.
     * @param  {Any} target - this target will become subscribable
     */
    this.copyProperties = function(target){
      for(var i in this)target[i] = this[i];
      target._clonedFrom = this;
      return target;
    };

    /**
     * Extend via properties
     * @param  {[type]} properties [description]
     * @return {[type]}            [description]
     */
    this.extend = function(properties){
      for(var i in properties)this[i] = properties[i];
      return this;
    }

    /**
     * Returns the number of subscriptions for the specific event name.
     * @param {String} event - if falsy; all subscriptions are counted
     * @return {Integer} Nr. of subscriptions on this object
     */
    this.getSubscriptionsCount = function(event){
      var count = 0, dependencies = this.getDependencies();
      if(event)return (dependencies[event]||[]).length;
      for(var i in dependencies)count += dependencies[i].length;
      return count;
    }
  }

  var stack = [];
  function recordDependency(dependency){
    var length = stack.length;
    var last = length > 0 ? stack[length-1] : [];
    if(last.observable !== dependency)last.push(dependency);
  }
  function recordExecution(func, context, observable){
    if(observable.recording || !func)return observable.value;
    var result = [];
    result.observable = observable;
    observable.recording = true;
    stack.push(result);
    try{
      return func.apply(context);
    }finally{
      stack.pop();
      observable.recording = false;
      for(var j = 0; j < (observable._subs||[]).length; j++){
        observable._subs[j].dispose();
      }
      observable._subs = [];
      for(var i = 0; i < result.length; i++){
        observable._subs.push(result[i].subscribe(function(){
          observable.peek();
        }));
      }
    }
  }

  function Observable(){

    function self(){
      if(arguments.length > 0) {
        self.setter(arguments[0], true, arguments);
        return this;
      }else{
        return self.getter();
      }
    };

    self.valueHasMutated = function(){
      self.notifySubscribers(self.value);

    };

    self.valueWillMutate = function(){
      self.notifySubscribers(self.value, "beforeChange");
    };

    self.setter = function(value, write, args){
      var compare = self.equalityComparer;
      if(self.notify === 'always' || !compare || !compare(value, self.value)){
        try{
          self.valueWillMutate();
          if(write && !self.write)throw "This observable can't be set.";
          self.value = value;
          if(write)self.write.apply(self.getContext(), args || value);
        }finally{
          self.valueHasMutated();
        }
      }
    };

    self.equalityComparer = function(newvalue, oldvalue){
      var isObject = Object.prototype.toString.call(newvalue) === "[object Object]";
      return !isObject && newvalue === oldvalue;
    };

    self.getter = function(){
      recordDependency(self);
      return self.value;
    };

    self.peek = function(){
      var value = recordExecution(self.read, self.getContext(), self);
      self.setter(value);
      return value;
    };

    self.getContext = function(){
      return self.owner === undefined ? global : self.owner;
    };

    /**
     * Inheritance
     */

    this.copyProperties(self);
    self.original = this;
    return self;
  }

  function DependentObservable(options, owner){

    if(!options || typeof (options.read || options) !== "function"){
      throw "Function has to be passed."
    }

    if (!(this instanceof DependentObservable)){
      return new DependentObservable(options, owner);
    }

    var self = Observable.call(this);

    if(options.read){
      self.extend(options);
      if(!self.owner)self.owner = owner;
    }else{
      self.read = options;
      self.owner = owner;
    }

    if(!self.deferEvaluation)self.peek();

    return self;
  }

  /**
   * Utility function that is used to build type checking functions. The resulting
   * function also checks for necessary properties and also returns true if the
   * copyProperties function was used to instantiate this object.
   *
   * @param  {Constructor} type to be checked against
   * @return {Function}
   */
  function buildCheckType(type, check){
    return function(any){
      return !!any
          && ( any instanceof type || any._clonedFrom instanceof type )
          && (!check || check(any));
    };
  }

  /**
   * Return a result object
   */
  global.ko = {
    subscribable: Subscribable,
    isSubscribable: buildCheckType(Subscribable),
    observable: function(initial){
      var self = new Observable();
      self.value = initial;
      self.write = function(){};
      return self;
    },
    dependentObservable: DependentObservable,
    computed: DependentObservable,
    isComputed: buildCheckType(DependentObservable),
    isObservable: buildCheckType(Observable),
    isWriteableObservable: buildCheckType(Observable, function(self){return !!self.write;})
  };

  /**
   * Prototype hirarchy
   */
  Subscribable.fn = Subscribable.prototype = {};
  global.ko.observable.fn = Observable.prototype = new Subscribable();
  DependentObservable.prototype = new Observable().original;

})(window);

      /*defer  = options.deferEvaluation ,
      owner  = options.owner,
      type   = options.type;*/ // computed, array on result

  //result.writable when !!setter

  // dont chnage when newValue === oldValue
  // see notify 'always' propertie on observable
  // add extend shim to qunit.html
  // change for equal object except when equalityComparer === func && return true
  //
  // result.notifySubscribers (can be set) resolved for every call
  //
  //  ko.subscribable.fn or ko.observable.fn musst be inherited
  //
  //  chaning syntax on write calls

  // beforeChange send event

<|endoftext|>