<commit_before>/*!

    Copyright (c) 2011 Peter van der Spek

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.

 */

(function($) {

    /**
     * Hash containing mapping of selectors to settings hashes for target selectors that should be live updated.
     *
     * @type {Object.<string, Object>}
     * @private
     */
    var liveUpdatingTargetSelectors = {};

    /**
     * Interval ID for live updater. Contains interval ID when the live updater interval is active, or is undefined
     * otherwise.
     *
     * @type {number}
     * @private
     */
    var liveUpdaterIntervalId;

    /**
     * Boolean indicating whether the live updater is running.
     *
     * @type {boolean}
     * @private
     */
    var liveUpdaterRunning = false;

    /**
     * Set of default settings.
     *
     * @type {Object.<string, string>}
     * @private
     */
    var defaultSettings = {
                ellipsis: '...',
                setTitle: 'never',
                live: false
            };

    /**
     * Perform ellipsis on selected elements.
     *
     * @param {string} selector the inner selector of elements that ellipsis may work on. Inner elements not referred to by this
     *      selector are left untouched.
     * @param {Object.<string, string>=} options optional options to override default settings.
     * @return {jQuery} the current jQuery object for chaining purposes.
     * @this {jQuery} the current jQuery object.
     */
    $.fn.ellipsis = function(selector, options) {
        var subjectElements, settings;

        subjectElements = $(this);

        // Check for options argument only.
        if (typeof selector !== 'string') {
            options = selector;
            selector = undefined;
        }

        // Create the settings from the given options and the default settings.
        settings = $.extend({}, defaultSettings, options);

        // If selector is not set, work on immediate children (default behaviour).
        settings.selector = selector;

        // Do ellipsis on each subject element.
        subjectElements.each(function() {
            var elem = $(this);

            // Do ellipsis on subject element.
            ellipsisOnElement(elem, settings);
        });

        // If live option is enabled, add subject elements to live updater. Otherwise remove from live updater.
        if (settings.live) {
            addToLiveUpdater(subjectElements.selector, settings);

        } else {
            removeFromLiveUpdater(subjectElements.selector);
        }

        // Return jQuery object for chaining.
        return this;
    };

    /**
     * Perform ellipsis on the given container.
     *
     * @param {jQuery} containerElement jQuery object containing one DOM element to perform ellipsis on.
     * @param {Object.<string, string>} settings the settings for this ellipsis operation.
     * @private
     */
    function ellipsisOnElement(containerElement, settings) {
        var containerData = containerElement.data();

        // Check if wrapper div was already created and bound to the container element.
        var wrapperElement = containerData.wrapperElement;

        // If not, create wrapper element.
        if (!wrapperElement) {
            wrapperElement = containerElement.wrapInner('<div/>').find('>div');
        }

        // Check if the original wrapper element content was already bound to the wrapper element.
        var wrapperElementData = wrapperElement.data();
        var wrapperOriginalContent = wrapperElementData.originalContent;

        // If so, clone the original content, re-bind the original wrapper content to the clone, and replace the
        // wrapper with the clone.
        if (wrapperOriginalContent) {
            wrapperElement = wrapperElementData.originalContent.clone(true)
                    .data({originalContent: wrapperOriginalContent}).replaceAll(wrapperElement);

        } else {
            // Otherwise, clone the current wrapper element and bind it as original content to the wrapper element.

            wrapperElement.data({originalContent: wrapperElement.clone(true)});
        }

        // Bind the wrapper element and current container width and height to the container element. Current container
        // width and height are stored to detect changes to the container size.
        containerElement.data({
            wrapperElement: wrapperElement,
            containerWidth: containerElement.innerWidth(),
            containerHeight: containerElement.innerHeight()
        });

        // Normally the ellipsis characters are applied to the last non-empty text-node in the selected element. If the
        // selected element becomes empty during ellipsis iteration, the ellipsis characters cannot be applied to that
        // selected element, and must be deferred to the previous selected element. This parameter keeps track of that.
        var deferAppendEllipsis = false;

        // Loop through all selected elements in reverse order.
        var selectedElements = wrapperElement;
        if (settings.selector) selectedElements = $(wrapperElement.find(settings.selector).get().reverse());

        selectedElements.each(function() {
            var selectedElement = $(this),
                    originalText = selectedElement.text(),
                    ellipsisApplied = false;

            // Check if we can safely remove the selected element. This saves a lot of unnecessary iterations.
            if (wrapperElement.innerHeight() - selectedElement.innerHeight() > containerElement.innerHeight()) {
                selectedElement.remove();

            } else {
                // Reverse recursively remove empty elements, until the element that contains a non-empty text-node.
                removeLastEmptyElements(selectedElement);

                // If the selected element has not become empty, start ellipsis iterations on the selected element.
                if (selectedElement.contents().length) {

                    // If a deffered ellipsis is still pending, apply it now to the last text-node.
                    if (deferAppendEllipsis) {
                        getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;
                        deferAppendEllipsis = false;
                    }

                    // Iterate until wrapper element height is less than or equal to the container element height.
                    while (wrapperElement.innerHeight() > containerElement.innerHeight()) {
                        // Apply ellipsis on last text node, by removing one word.
                        ellipsisApplied = ellipsisOnLastTextNode(selectedElement);

                        // If ellipsis was succesfully applied, remove any remaining empty last elements and append the
                        // ellipsis characters.
                        if (ellipsisApplied) {
                            removeLastEmptyElements(selectedElement);

                            // If the selected element is not empty, append the ellipsis characters.
                            if (selectedElement.contents().length) {
                                getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;

                            } else {
                                // If the selected element has become empty, defer the appending of the ellipsis characters
                                // to the previous selected element.
                                deferAppendEllipsis = true;
                                selectedElement.remove();
                                break;
                            }

                        } else {
                            // If ellipsis could not be applied, defer the appending of the ellipsis characters to the
                            // previous selected element.
                            deferAppendEllipsis = true;
                            selectedElement.remove();
                            break;
                        }
                    }

                    // If the "setTitle" property is set to "onEllipsis" and the ellipsis has been applied, or if the
                    // property is set to "always", the add the "title" attribute with the original text. Else remove the
                    // "title" attribute. When the "setTitle" property is set to "never" we do not touch the "title"
                    // attribute.
                    if (((settings.setTitle == 'onEllipsis') && ellipsisApplied) || (settings.setTitle == 'always')) {
                        selectedElement.attr('title', originalText);

                    } else if (settings.setTitle != 'never') {
                        selectedElement.removeAttr('title');
                    }
                }
            }
        });
    }

    /**
     * Performs ellipsis on the last text node of the given element. Ellipsis is done by removing a full word.
     *
     * @param {jQuery} element jQuery object containing a single DOM element.
     * @return {boolean} true when ellipsis has been done, false otherwise.
     * @private
     */
    function ellipsisOnLastTextNode(element) {
        var lastTextNode = getLastTextNode(element);

        // If the last text node is found, do ellipsis on that node.
        if (lastTextNode.length) {
            var text = lastTextNode.get(0).nodeValue;

            // Find last space character, and remove text from there. If no space is found the full remaining text is
            // removed.
            var pos = text.lastIndexOf(' ');
            if (pos > -1) {
                text = $.trim(text.substring(0, pos));
                lastTextNode.get(0).nodeValue = text;

            } else {
                lastTextNode.get(0).nodeValue = '';
            }

            return true;
        }

        return false;
    }

    /**
     * Get last text node of the given element.
     *
     * @param {jQuery} element jQuery object containing a single element.
     * @return {jQuery} jQuery object containing a single text node.
     * @private
     */
    function getLastTextNode(element) {
        if (element.contents().length) {

            // Get last child node.
            var lastNode = element.contents().last();

            // If last node is a text node, return it.
            if (lastNode.filter(textNodeFilter).length) {
                return lastNode;

            } else {
                // Else it is an element node, and we recurse into it.

                return getLastTextNode(lastNode);
            }

        } else {
            // If there is no last child node, we append an empty text node and return that. Normally this should not
            // happen, as we test for emptiness before calling getLastTextNode.

            element.append('');
            return element.contents().last();
        }
    }

    /**
     * Remove last empty elements. This is done recursively until the last element contains a non-empty text node.
     *
     * @param {jQuery} element jQuery object containing a single element.
     * @return {boolean} true when elements have been removed, false otherwise.
     * @private
     */
    function removeLastEmptyElements(element) {
        if (element.contents().length) {

            // Get last child node.
            var lastNode = element.contents().last();

            // If last child node is a text node, check for emptiness.
            if (lastNode.filter(textNodeFilter).length) {
                var text = lastNode.get(0).nodeValue;
                text = $.trim(text);

                if (text == '') {
                    // If empty, remove the text node.
                    lastNode.remove();

                    return true;

                } else {
                    return false;
                }

            } else {
                // If the last child node is an element node, remove the last empty child nodes on that node.
                while (removeLastEmptyElements(lastNode)) {
                }

                // If the last child node contains no more child nodes, remove the last child node.
                if (lastNode.contents().length) {
                    return false;

                } else {
                    lastNode.remove();

                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Filter for testing on text nodes.
     *
     * @return {boolean} true when this node is a text node, false otherwise.
     * @this {Node}
     * @private
     */
    function textNodeFilter() {
        return this.nodeType === 3;
    }

    /**
     * Add target selector to hash of target selectors. If this is the first target selector added, start the live
     * updater.
     *
     * @param {string} targetSelector the target selector to run the live updater for.
     * @param {Object.<string, string>} settings the settings to apply on this target selector.
     * @private
     */
    function addToLiveUpdater(targetSelector, settings) {
        // Store target selector with its settings.
        liveUpdatingTargetSelectors[targetSelector] = settings;

        // If the live updater has not yet been started, start it now.
        if (!liveUpdaterIntervalId) {
            liveUpdaterIntervalId = window.setInterval(function() {
                doLiveUpdater();
            }, 200);
        }
    }

    /**
     * Remove the target selector from the hash of target selectors. It this is the last remaining target selector
     * being removed, stop the live updater.
     *
     * @param {string} targetSelector the target selector to stop running the live updater for.
     * @private
     */
    function removeFromLiveUpdater(targetSelector) {
        // If the hash contains the target selector, remove it.
        if (liveUpdatingTargetSelectors[targetSelector]) {
            delete liveUpdatingTargetSelectors[targetSelector];

            // If no more target selectors are in the hash, stop the live updater.
            if (!liveUpdatingTargetSelectors.length) {
                if (liveUpdaterIntervalId) {
                    window.clearInterval(liveUpdaterIntervalId);
                    liveUpdaterIntervalId = undefined;
                }
            }
        }
    };

    /**
     * Run the live updater. The live updater is periodically run to check if its monitored target selectors require
     * re-applying of the ellipsis.
     *
     * @private
     */
    function doLiveUpdater() {
        // If the live updater is already running, skip this time. We only want one instance running at a time.
        if (!liveUpdaterRunning) {
            liveUpdaterRunning = true;

            // Loop through target selectors.
            for (var targetSelector in liveUpdatingTargetSelectors) {
                $(targetSelector).each(function() {
                    var containerElement, containerData;

                    containerElement = $(this);
                    containerData = containerElement.data();

                    // If container element dimensions have changed, or the container element is new, run ellipsis on
                    // that container element.
                    if ((containerData.containerWidth != containerElement.innerWidth()) ||
                            (containerData.containerHeight != containerElement.innerHeight())) {
                        ellipsisOnElement(containerElement, liveUpdatingTargetSelectors[targetSelector]);
                    }
                });
            }

            liveUpdaterRunning = false;
        }
    };

})(jQuery);<commit_msg>Fixed handling of possible undefined return value. Closes #5<commit_after>/*!

    Copyright (c) 2011 Peter van der Spek

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.

 */

(function($) {

    /**
     * Hash containing mapping of selectors to settings hashes for target selectors that should be live updated.
     *
     * @type {Object.<string, Object>}
     * @private
     */
    var liveUpdatingTargetSelectors = {};

    /**
     * Interval ID for live updater. Contains interval ID when the live updater interval is active, or is undefined
     * otherwise.
     *
     * @type {number}
     * @private
     */
    var liveUpdaterIntervalId;

    /**
     * Boolean indicating whether the live updater is running.
     *
     * @type {boolean}
     * @private
     */
    var liveUpdaterRunning = false;

    /**
     * Set of default settings.
     *
     * @type {Object.<string, string>}
     * @private
     */
    var defaultSettings = {
                ellipsis: '...',
                setTitle: 'never',
                live: false
            };

    /**
     * Perform ellipsis on selected elements.
     *
     * @param {string} selector the inner selector of elements that ellipsis may work on. Inner elements not referred to by this
     *      selector are left untouched.
     * @param {Object.<string, string>=} options optional options to override default settings.
     * @return {jQuery} the current jQuery object for chaining purposes.
     * @this {jQuery} the current jQuery object.
     */
    $.fn.ellipsis = function(selector, options) {
        var subjectElements, settings;

        subjectElements = $(this);

        // Check for options argument only.
        if (typeof selector !== 'string') {
            options = selector;
            selector = undefined;
        }

        // Create the settings from the given options and the default settings.
        settings = $.extend({}, defaultSettings, options);

        // If selector is not set, work on immediate children (default behaviour).
        settings.selector = selector;

        // Do ellipsis on each subject element.
        subjectElements.each(function() {
            var elem = $(this);

            // Do ellipsis on subject element.
            ellipsisOnElement(elem, settings);
        });

        // If live option is enabled, add subject elements to live updater. Otherwise remove from live updater.
        if (settings.live) {
            addToLiveUpdater(subjectElements.selector, settings);

        } else {
            removeFromLiveUpdater(subjectElements.selector);
        }

        // Return jQuery object for chaining.
        return this;
    };

    /**
     * Perform ellipsis on the given container.
     *
     * @param {jQuery} containerElement jQuery object containing one DOM element to perform ellipsis on.
     * @param {Object.<string, string>} settings the settings for this ellipsis operation.
     * @private
     */
    function ellipsisOnElement(containerElement, settings) {
        var containerData = containerElement.data();

        // data() might return undefined (at least according to jQuery docs)
        if (!containerData) containerData = {};

        // Check if wrapper div was already created and bound to the container element.
        var wrapperElement = containerData.wrapperElement;

        // If not, create wrapper element.
        if (!wrapperElement) {
            wrapperElement = containerElement.wrapInner('<div/>').find('>div');
        }

        // Check if the original wrapper element content was already bound to the wrapper element.
        var wrapperElementData = wrapperElement.data();
        var wrapperOriginalContent = wrapperElementData.originalContent;

        // If so, clone the original content, re-bind the original wrapper content to the clone, and replace the
        // wrapper with the clone.
        if (wrapperOriginalContent) {
            wrapperElement = wrapperElementData.originalContent.clone(true)
                    .data({originalContent: wrapperOriginalContent}).replaceAll(wrapperElement);

        } else {
            // Otherwise, clone the current wrapper element and bind it as original content to the wrapper element.

            wrapperElement.data({originalContent: wrapperElement.clone(true)});
        }

        // Bind the wrapper element and current container width and height to the container element. Current container
        // width and height are stored to detect changes to the container size.
        containerElement.data({
            wrapperElement: wrapperElement,
            containerWidth: containerElement.innerWidth(),
            containerHeight: containerElement.innerHeight()
        });

        // Normally the ellipsis characters are applied to the last non-empty text-node in the selected element. If the
        // selected element becomes empty during ellipsis iteration, the ellipsis characters cannot be applied to that
        // selected element, and must be deferred to the previous selected element. This parameter keeps track of that.
        var deferAppendEllipsis = false;

        // Loop through all selected elements in reverse order.
        var selectedElements = wrapperElement;
        if (settings.selector) selectedElements = $(wrapperElement.find(settings.selector).get().reverse());

        selectedElements.each(function() {
            var selectedElement = $(this),
                    originalText = selectedElement.text(),
                    ellipsisApplied = false;

            // Check if we can safely remove the selected element. This saves a lot of unnecessary iterations.
            if (wrapperElement.innerHeight() - selectedElement.innerHeight() > containerElement.innerHeight()) {
                selectedElement.remove();

            } else {
                // Reverse recursively remove empty elements, until the element that contains a non-empty text-node.
                removeLastEmptyElements(selectedElement);

                // If the selected element has not become empty, start ellipsis iterations on the selected element.
                if (selectedElement.contents().length) {

                    // If a deffered ellipsis is still pending, apply it now to the last text-node.
                    if (deferAppendEllipsis) {
                        getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;
                        deferAppendEllipsis = false;
                    }

                    // Iterate until wrapper element height is less than or equal to the container element height.
                    while (wrapperElement.innerHeight() > containerElement.innerHeight()) {
                        // Apply ellipsis on last text node, by removing one word.
                        ellipsisApplied = ellipsisOnLastTextNode(selectedElement);

                        // If ellipsis was succesfully applied, remove any remaining empty last elements and append the
                        // ellipsis characters.
                        if (ellipsisApplied) {
                            removeLastEmptyElements(selectedElement);

                            // If the selected element is not empty, append the ellipsis characters.
                            if (selectedElement.contents().length) {
                                getLastTextNode(selectedElement).get(0).nodeValue += settings.ellipsis;

                            } else {
                                // If the selected element has become empty, defer the appending of the ellipsis characters
                                // to the previous selected element.
                                deferAppendEllipsis = true;
                                selectedElement.remove();
                                break;
                            }

                        } else {
                            // If ellipsis could not be applied, defer the appending of the ellipsis characters to the
                            // previous selected element.
                            deferAppendEllipsis = true;
                            selectedElement.remove();
                            break;
                        }
                    }

                    // If the "setTitle" property is set to "onEllipsis" and the ellipsis has been applied, or if the
                    // property is set to "always", the add the "title" attribute with the original text. Else remove the
                    // "title" attribute. When the "setTitle" property is set to "never" we do not touch the "title"
                    // attribute.
                    if (((settings.setTitle == 'onEllipsis') && ellipsisApplied) || (settings.setTitle == 'always')) {
                        selectedElement.attr('title', originalText);

                    } else if (settings.setTitle != 'never') {
                        selectedElement.removeAttr('title');
                    }
                }
            }
        });
    }

    /**
     * Performs ellipsis on the last text node of the given element. Ellipsis is done by removing a full word.
     *
     * @param {jQuery} element jQuery object containing a single DOM element.
     * @return {boolean} true when ellipsis has been done, false otherwise.
     * @private
     */
    function ellipsisOnLastTextNode(element) {
        var lastTextNode = getLastTextNode(element);

        // If the last text node is found, do ellipsis on that node.
        if (lastTextNode.length) {
            var text = lastTextNode.get(0).nodeValue;

            // Find last space character, and remove text from there. If no space is found the full remaining text is
            // removed.
            var pos = text.lastIndexOf(' ');
            if (pos > -1) {
                text = $.trim(text.substring(0, pos));
                lastTextNode.get(0).nodeValue = text;

            } else {
                lastTextNode.get(0).nodeValue = '';
            }

            return true;
        }

        return false;
    }

    /**
     * Get last text node of the given element.
     *
     * @param {jQuery} element jQuery object containing a single element.
     * @return {jQuery} jQuery object containing a single text node.
     * @private
     */
    function getLastTextNode(element) {
        if (element.contents().length) {

            // Get last child node.
            var lastNode = element.contents().last();

            // If last node is a text node, return it.
            if (lastNode.filter(textNodeFilter).length) {
                return lastNode;

            } else {
                // Else it is an element node, and we recurse into it.

                return getLastTextNode(lastNode);
            }

        } else {
            // If there is no last child node, we append an empty text node and return that. Normally this should not
            // happen, as we test for emptiness before calling getLastTextNode.

            element.append('');
            return element.contents().last();
        }
    }

    /**
     * Remove last empty elements. This is done recursively until the last element contains a non-empty text node.
     *
     * @param {jQuery} element jQuery object containing a single element.
     * @return {boolean} true when elements have been removed, false otherwise.
     * @private
     */
    function removeLastEmptyElements(element) {
        if (element.contents().length) {

            // Get last child node.
            var lastNode = element.contents().last();

            // If last child node is a text node, check for emptiness.
            if (lastNode.filter(textNodeFilter).length) {
                var text = lastNode.get(0).nodeValue;
                text = $.trim(text);

                if (text == '') {
                    // If empty, remove the text node.
                    lastNode.remove();

                    return true;

                } else {
                    return false;
                }

            } else {
                // If the last child node is an element node, remove the last empty child nodes on that node.
                while (removeLastEmptyElements(lastNode)) {
                }

                // If the last child node contains no more child nodes, remove the last child node.
                if (lastNode.contents().length) {
                    return false;

                } else {
                    lastNode.remove();

                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Filter for testing on text nodes.
     *
     * @return {boolean} true when this node is a text node, false otherwise.
     * @this {Node}
     * @private
     */
    function textNodeFilter() {
        return this.nodeType === 3;
    }

    /**
     * Add target selector to hash of target selectors. If this is the first target selector added, start the live
     * updater.
     *
     * @param {string} targetSelector the target selector to run the live updater for.
     * @param {Object.<string, string>} settings the settings to apply on this target selector.
     * @private
     */
    function addToLiveUpdater(targetSelector, settings) {
        // Store target selector with its settings.
        liveUpdatingTargetSelectors[targetSelector] = settings;

        // If the live updater has not yet been started, start it now.
        if (!liveUpdaterIntervalId) {
            liveUpdaterIntervalId = window.setInterval(function() {
                doLiveUpdater();
            }, 200);
        }
    }

    /**
     * Remove the target selector from the hash of target selectors. It this is the last remaining target selector
     * being removed, stop the live updater.
     *
     * @param {string} targetSelector the target selector to stop running the live updater for.
     * @private
     */
    function removeFromLiveUpdater(targetSelector) {
        // If the hash contains the target selector, remove it.
        if (liveUpdatingTargetSelectors[targetSelector]) {
            delete liveUpdatingTargetSelectors[targetSelector];

            // If no more target selectors are in the hash, stop the live updater.
            if (!liveUpdatingTargetSelectors.length) {
                if (liveUpdaterIntervalId) {
                    window.clearInterval(liveUpdaterIntervalId);
                    liveUpdaterIntervalId = undefined;
                }
            }
        }
    };

    /**
     * Run the live updater. The live updater is periodically run to check if its monitored target selectors require
     * re-applying of the ellipsis.
     *
     * @private
     */
    function doLiveUpdater() {
        // If the live updater is already running, skip this time. We only want one instance running at a time.
        if (!liveUpdaterRunning) {
            liveUpdaterRunning = true;

            // Loop through target selectors.
            for (var targetSelector in liveUpdatingTargetSelectors) {
                $(targetSelector).each(function() {
                    var containerElement, containerData;

                    containerElement = $(this);
                    containerData = containerElement.data();

                    // If container element dimensions have changed, or the container element is new, run ellipsis on
                    // that container element.
                    if ((containerData.containerWidth != containerElement.innerWidth()) ||
                            (containerData.containerHeight != containerElement.innerHeight())) {
                        ellipsisOnElement(containerElement, liveUpdatingTargetSelectors[targetSelector]);
                    }
                });
            }

            liveUpdaterRunning = false;
        }
    };

})(jQuery);<|endoftext|>


<commit_before>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

/* eslint-disable global-require */

import React from 'react';
import PropTypes from 'prop-types';

import { connect } from 'react-redux';

import {
  BackHandler,
  Image,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
} from 'react-native';

import { Scheduler } from 'sussol-utilities';
import { NavigationActions } from 'react-navigation';
import globalStyles, {
  dataTableColors,
  dataTableStyles,
  pageStyles,
  textStyles,
  SUSSOL_ORANGE,
} from './globalStyles';

import { FirstUsePage, FINALISABLE_PAGES } from './pages';
import { MobileAppSettings } from './settings';
import { Synchroniser, PostSyncProcessor, SyncModal } from './sync';
import {
  FinaliseButton,
  FinaliseModal,
  LoginModal,
  NavigationBar,
  SyncState,
  Spinner,
} from './widgets';

import { getCurrentParams, getCurrentRouteName } from './navigation';
import { migrateDataToVersion } from './dataMigration';
import { SyncAuthenticator, UserAuthenticator } from './authentication';
import { Database, schema, UIDatabase } from './database';
import ReduxNavigator from './navigation/ReduxNavigation';

const SYNC_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds.
const AUTHENTICATION_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds.

class MSupplyMobileAppContainer extends React.Component {
  constructor(props, ...otherArgs) {
    super(props, ...otherArgs);
    const database = new Database(schema);
    this.database = new UIDatabase(database);
    this.settings = new MobileAppSettings(this.database);
    migrateDataToVersion(this.database, this.settings);
    this.userAuthenticator = new UserAuthenticator(this.database, this.settings);
    const syncAuthenticator = new SyncAuthenticator(this.settings);
    this.synchroniser = new Synchroniser(
      database,
      syncAuthenticator,
      this.settings,
      props.dispatch
    );
    this.postSyncProcessor = new PostSyncProcessor(this.database, this.settings);
    this.scheduler = new Scheduler();
    const isInitialised = this.synchroniser.isInitialised();
    this.scheduler.schedule(this.synchronise, SYNC_INTERVAL);
    this.scheduler.schedule(() => {
      const { currentUser } = this.state;
      if (currentUser !== null) {
        // Only reauthenticate if currently logged in.
        this.userAuthenticator.reauthenticate(this.onAuthentication);
      }
    }, AUTHENTICATION_INTERVAL);
    this.state = {
      confirmFinalise: false,
      currentUser: null,
      isInitialised,
      isLoading: false,
      syncModalIsOpen: false,
    };
  }

  componentDidMount = () => BackHandler.addEventListener('hardwareBackPress', this.handleBackEvent);

  componentWillUnmount = () => {
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackEvent);
    this.scheduler.clearAll();
  };

  onAuthentication = user => {
    this.setState({ currentUser: user });
    this.postSyncProcessor.setUser(user);
  };

  onInitialised = () => {
    this.setState({ isInitialised: true });
    this.postSyncProcessor.processAnyUnprocessedRecords();
  };

  getCanNavigateBack = () => {
    const { navigationState } = this.props;
    return this.navigator && navigationState.index !== 0;
  };

  handleBackEvent = () => {
    const { confirmFinalise, syncModalIsOpen } = this.state;
    // If finalise or sync modals are open, close them rather than navigating.
    if (confirmFinalise || syncModalIsOpen) {
      this.setState({ confirmFinalise: false, syncModalIsOpen: false });
      return true;
    }
    // If we are on base screen (e.g. home), back button should close app as we can't go back.
    if (!this.getCanNavigateBack()) BackHandler.exitApp();
    else {
      const { dispatch } = this.navigator.props;
      dispatch(NavigationActions.back());
    }
    return true;
  };

  runWithLoadingIndicator = async functionToRun => {
    this.database.isLoading = true;
    // We here set up an asyncronous promise that will be resolved after a timeout
    // of 1 millisecond. This allows a fraction of a delay for the javascript thread
    // to unblock and allow the spinner animation to start up. The |functionToRun| should
    // not be run inside a |setTimeout| as that relegates to a lower priority, resulting
    // in very slow performance.
    await new Promise(resolve => {
      this.setState({ isLoading: true }, () => setTimeout(resolve, 1));
    });
    functionToRun();
    this.setState({ isLoading: false });
    this.database.isLoading = false;
  };

  synchronise = async () => {
    const { syncState } = this.props;
    const { isInitialised } = this.state;
    if (!isInitialised || syncState.isSyncing) return; // Ignore if syncing.
    // True if most recent call to |this.synchroniser.synchronise()| failed.
    const lastSyncFailed = this.synchroniser.lastSyncFailed();
    const lastPostSyncProcessingFailed = this.postSyncProcessor.lastPostSyncProcessingFailed();
    await this.synchroniser.synchronise();
    if (lastSyncFailed || lastPostSyncProcessingFailed) {
      // If last sync was interrupted, it did not enter this block. If the app was closed, it did
      // not store the records left in the record queue, so tables should be checked for unprocessed
      // records. If the last processing of the record queue was interrupted by app crash then all
      // records need to be checked.
      this.postSyncProcessor.processAnyUnprocessedRecords();
    } else {
      this.postSyncProcessor.processRecordQueue();
    }
  };

  logOut = () => this.setState({ currentUser: null });

  renderFinaliseButton = () => {
    const { finaliseItem } = this.props;
    return (
      <FinaliseButton
        isFinalised={finaliseItem.record.isFinalised}
        onPress={() => this.setState({ confirmFinalise: true })}
      />
    );
  };

  renderLogo = () => {
    const { isInAdminMode } = this.state;

    return (
      <TouchableWithoutFeedback
        delayLongPress={3000}
        onLongPress={() => this.setState({ isInAdminMode: !isInAdminMode })}
      >
        <Image resizeMode="contain" source={require('./images/logo.png')} />
      </TouchableWithoutFeedback>
    );
  };

  renderLoadingIndicator = () => {
    const { isLoading } = this.state;
    return (
      <View style={globalStyles.loadingIndicatorContainer}>
        <Spinner isSpinning={isLoading} color={SUSSOL_ORANGE} />
      </View>
    );
  };

  renderPageTitle = () => {
    const { currentTitle } = this.props;
    return <Text style={textStyles}>{currentTitle}</Text>;
  };

  renderSyncState = () => {
    const { syncState } = this.props;
    return (
      <TouchableOpacity
        style={{ flexDirection: 'row' }}
        onPress={() => this.setState({ syncModalIsOpen: true })}
      >
        <SyncState state={syncState} />
      </TouchableOpacity>
    );
  };

  render() {
    const { dispatch, finaliseItem, navigationState, syncState } = this.props;

    const {
      confirmFinalise,
      currentUser,
      isInAdminMode,
      isInitialised,
      isLoading,
      syncModalIsOpen,
    } = this.state;

    if (!isInitialised) {
      return (
        <FirstUsePage
          synchroniser={this.synchroniser}
          onInitialised={this.onInitialised}
          syncState={syncState}
        />
      );
    }

    return (
      <View style={globalStyles.appBackground}>
        <NavigationBar
          onPressBack={this.getCanNavigateBack() ? this.handleBackEvent : null}
          LeftComponent={this.getCanNavigateBack() ? this.renderPageTitle : null}
          CentreComponent={this.renderLogo}
          RightComponent={finaliseItem ? this.renderFinaliseButton : this.renderSyncState}
        />

        <ReduxNavigator
          ref={navigator => {
            this.navigator = navigator;
          }}
          state={navigationState}
          dispatch={dispatch}
          screenProps={{
            database: this.database,
            settings: this.settings,
            logOut: this.logOut,
            currentUser,
            runWithLoadingIndicator: this.runWithLoadingIndicator,
            isInAdminMode,
            genericTablePageStyles: {
              searchBarColor: SUSSOL_ORANGE,
              dataTableStyles,
              pageStyles,
              colors: dataTableColors,
            },
          }}
        />
        <FinaliseModal
          database={this.database}
          isOpen={confirmFinalise}
          onClose={() => this.setState({ confirmFinalise: false })}
          finaliseItem={finaliseItem}
          user={currentUser}
          runWithLoadingIndicator={this.runWithLoadingIndicator}
        />
        <SyncModal
          database={this.database}
          isOpen={syncModalIsOpen}
          state={syncState}
          onPressManualSync={this.synchronise}
          onClose={() => this.setState({ syncModalIsOpen: false })}
        />
        <LoginModal
          authenticator={this.userAuthenticator}
          settings={this.settings}
          isAuthenticated={currentUser !== null}
          onAuthentication={this.onAuthentication}
        />
        {isLoading && this.renderLoadingIndicator()}
      </View>
    );
  }
}

/* eslint-disable react/require-default-props, react/forbid-prop-types */
MSupplyMobileAppContainer.propTypes = {
  currentTitle: PropTypes.string,
  dispatch: PropTypes.func.isRequired,
  finaliseItem: PropTypes.object,
  navigationState: PropTypes.object.isRequired,
  syncState: PropTypes.object.isRequired,
};

function mapStateToProps(state) {
  const { nav: navigationState, sync: syncState } = state;
  const currentParams = getCurrentParams(navigationState);
  const currentTitle = currentParams && currentParams.title;
  const finaliseItem = FINALISABLE_PAGES[getCurrentRouteName(navigationState)];
  if (finaliseItem && currentParams) {
    finaliseItem.record = currentParams[finaliseItem.recordToFinaliseKey];
  }

  return {
    currentTitle,
    finaliseItem,
    navigationState,
    syncState,
  };
}

export default connect(mapStateToProps)(MSupplyMobileAppContainer);
<commit_msg>Move mapStateToProps above propTypes declaration<commit_after>/**
 * mSupply Mobile
 * Sustainable Solutions (NZ) Ltd. 2019
 */

/* eslint-disable global-require */

import React from 'react';
import PropTypes from 'prop-types';

import { connect } from 'react-redux';

import {
  BackHandler,
  Image,
  Text,
  TouchableOpacity,
  TouchableWithoutFeedback,
  View,
} from 'react-native';

import { Scheduler } from 'sussol-utilities';
import { NavigationActions } from 'react-navigation';
import globalStyles, {
  dataTableColors,
  dataTableStyles,
  pageStyles,
  textStyles,
  SUSSOL_ORANGE,
} from './globalStyles';

import { FirstUsePage, FINALISABLE_PAGES } from './pages';
import { MobileAppSettings } from './settings';
import { Synchroniser, PostSyncProcessor, SyncModal } from './sync';
import {
  FinaliseButton,
  FinaliseModal,
  LoginModal,
  NavigationBar,
  SyncState,
  Spinner,
} from './widgets';

import { getCurrentParams, getCurrentRouteName } from './navigation';
import { migrateDataToVersion } from './dataMigration';
import { SyncAuthenticator, UserAuthenticator } from './authentication';
import { Database, schema, UIDatabase } from './database';
import ReduxNavigator from './navigation/ReduxNavigation';

const SYNC_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds.
const AUTHENTICATION_INTERVAL = 10 * 60 * 1000; // 10 minutes in milliseconds.

class MSupplyMobileAppContainer extends React.Component {
  constructor(props, ...otherArgs) {
    super(props, ...otherArgs);
    const database = new Database(schema);
    this.database = new UIDatabase(database);
    this.settings = new MobileAppSettings(this.database);
    migrateDataToVersion(this.database, this.settings);
    this.userAuthenticator = new UserAuthenticator(this.database, this.settings);
    const syncAuthenticator = new SyncAuthenticator(this.settings);
    this.synchroniser = new Synchroniser(
      database,
      syncAuthenticator,
      this.settings,
      props.dispatch
    );
    this.postSyncProcessor = new PostSyncProcessor(this.database, this.settings);
    this.scheduler = new Scheduler();
    const isInitialised = this.synchroniser.isInitialised();
    this.scheduler.schedule(this.synchronise, SYNC_INTERVAL);
    this.scheduler.schedule(() => {
      const { currentUser } = this.state;
      if (currentUser !== null) {
        // Only reauthenticate if currently logged in.
        this.userAuthenticator.reauthenticate(this.onAuthentication);
      }
    }, AUTHENTICATION_INTERVAL);
    this.state = {
      confirmFinalise: false,
      currentUser: null,
      isInitialised,
      isLoading: false,
      syncModalIsOpen: false,
    };
  }

  componentDidMount = () => BackHandler.addEventListener('hardwareBackPress', this.handleBackEvent);

  componentWillUnmount = () => {
    BackHandler.removeEventListener('hardwareBackPress', this.handleBackEvent);
    this.scheduler.clearAll();
  };

  onAuthentication = user => {
    this.setState({ currentUser: user });
    this.postSyncProcessor.setUser(user);
  };

  onInitialised = () => {
    this.setState({ isInitialised: true });
    this.postSyncProcessor.processAnyUnprocessedRecords();
  };

  getCanNavigateBack = () => {
    const { navigationState } = this.props;
    return this.navigator && navigationState.index !== 0;
  };

  handleBackEvent = () => {
    const { confirmFinalise, syncModalIsOpen } = this.state;
    // If finalise or sync modals are open, close them rather than navigating.
    if (confirmFinalise || syncModalIsOpen) {
      this.setState({ confirmFinalise: false, syncModalIsOpen: false });
      return true;
    }
    // If we are on base screen (e.g. home), back button should close app as we can't go back.
    if (!this.getCanNavigateBack()) BackHandler.exitApp();
    else {
      const { dispatch } = this.navigator.props;
      dispatch(NavigationActions.back());
    }
    return true;
  };

  runWithLoadingIndicator = async functionToRun => {
    this.database.isLoading = true;
    // We here set up an asyncronous promise that will be resolved after a timeout
    // of 1 millisecond. This allows a fraction of a delay for the javascript thread
    // to unblock and allow the spinner animation to start up. The |functionToRun| should
    // not be run inside a |setTimeout| as that relegates to a lower priority, resulting
    // in very slow performance.
    await new Promise(resolve => {
      this.setState({ isLoading: true }, () => setTimeout(resolve, 1));
    });
    functionToRun();
    this.setState({ isLoading: false });
    this.database.isLoading = false;
  };

  synchronise = async () => {
    const { syncState } = this.props;
    const { isInitialised } = this.state;
    if (!isInitialised || syncState.isSyncing) return; // Ignore if syncing.
    // True if most recent call to |this.synchroniser.synchronise()| failed.
    const lastSyncFailed = this.synchroniser.lastSyncFailed();
    const lastPostSyncProcessingFailed = this.postSyncProcessor.lastPostSyncProcessingFailed();
    await this.synchroniser.synchronise();
    if (lastSyncFailed || lastPostSyncProcessingFailed) {
      // If last sync was interrupted, it did not enter this block. If the app was closed, it did
      // not store the records left in the record queue, so tables should be checked for unprocessed
      // records. If the last processing of the record queue was interrupted by app crash then all
      // records need to be checked.
      this.postSyncProcessor.processAnyUnprocessedRecords();
    } else {
      this.postSyncProcessor.processRecordQueue();
    }
  };

  logOut = () => this.setState({ currentUser: null });

  renderFinaliseButton = () => {
    const { finaliseItem } = this.props;
    return (
      <FinaliseButton
        isFinalised={finaliseItem.record.isFinalised}
        onPress={() => this.setState({ confirmFinalise: true })}
      />
    );
  };

  renderLogo = () => {
    const { isInAdminMode } = this.state;

    return (
      <TouchableWithoutFeedback
        delayLongPress={3000}
        onLongPress={() => this.setState({ isInAdminMode: !isInAdminMode })}
      >
        <Image resizeMode="contain" source={require('./images/logo.png')} />
      </TouchableWithoutFeedback>
    );
  };

  renderLoadingIndicator = () => {
    const { isLoading } = this.state;
    return (
      <View style={globalStyles.loadingIndicatorContainer}>
        <Spinner isSpinning={isLoading} color={SUSSOL_ORANGE} />
      </View>
    );
  };

  renderPageTitle = () => {
    const { currentTitle } = this.props;
    return <Text style={textStyles}>{currentTitle}</Text>;
  };

  renderSyncState = () => {
    const { syncState } = this.props;
    return (
      <TouchableOpacity
        style={{ flexDirection: 'row' }}
        onPress={() => this.setState({ syncModalIsOpen: true })}
      >
        <SyncState state={syncState} />
      </TouchableOpacity>
    );
  };

  render() {
    const { dispatch, finaliseItem, navigationState, syncState } = this.props;

    const {
      confirmFinalise,
      currentUser,
      isInAdminMode,
      isInitialised,
      isLoading,
      syncModalIsOpen,
    } = this.state;

    if (!isInitialised) {
      return (
        <FirstUsePage
          synchroniser={this.synchroniser}
          onInitialised={this.onInitialised}
          syncState={syncState}
        />
      );
    }

    return (
      <View style={globalStyles.appBackground}>
        <NavigationBar
          onPressBack={this.getCanNavigateBack() ? this.handleBackEvent : null}
          LeftComponent={this.getCanNavigateBack() ? this.renderPageTitle : null}
          CentreComponent={this.renderLogo}
          RightComponent={finaliseItem ? this.renderFinaliseButton : this.renderSyncState}
        />

        <ReduxNavigator
          ref={navigator => {
            this.navigator = navigator;
          }}
          state={navigationState}
          dispatch={dispatch}
          screenProps={{
            database: this.database,
            settings: this.settings,
            logOut: this.logOut,
            currentUser,
            runWithLoadingIndicator: this.runWithLoadingIndicator,
            isInAdminMode,
            genericTablePageStyles: {
              searchBarColor: SUSSOL_ORANGE,
              dataTableStyles,
              pageStyles,
              colors: dataTableColors,
            },
          }}
        />
        <FinaliseModal
          database={this.database}
          isOpen={confirmFinalise}
          onClose={() => this.setState({ confirmFinalise: false })}
          finaliseItem={finaliseItem}
          user={currentUser}
          runWithLoadingIndicator={this.runWithLoadingIndicator}
        />
        <SyncModal
          database={this.database}
          isOpen={syncModalIsOpen}
          state={syncState}
          onPressManualSync={this.synchronise}
          onClose={() => this.setState({ syncModalIsOpen: false })}
        />
        <LoginModal
          authenticator={this.userAuthenticator}
          settings={this.settings}
          isAuthenticated={currentUser !== null}
          onAuthentication={this.onAuthentication}
        />
        {isLoading && this.renderLoadingIndicator()}
      </View>
    );
  }
}

const mapStateToProps = state => {
  const { nav: navigationState, sync: syncState } = state;
  const currentParams = getCurrentParams(navigationState);
  const currentTitle = currentParams && currentParams.title;
  const finaliseItem = FINALISABLE_PAGES[getCurrentRouteName(navigationState)];
  if (finaliseItem && currentParams) {
    finaliseItem.record = currentParams[finaliseItem.recordToFinaliseKey];
  }

  return {
    currentTitle,
    finaliseItem,
    navigationState,
    syncState,
  };
};

/* eslint-disable react/require-default-props, react/forbid-prop-types */
MSupplyMobileAppContainer.propTypes = {
  currentTitle: PropTypes.string,
  dispatch: PropTypes.func.isRequired,
  finaliseItem: PropTypes.object,
  navigationState: PropTypes.object.isRequired,
  syncState: PropTypes.object.isRequired,
};

export default connect(mapStateToProps)(MSupplyMobileAppContainer);
<|endoftext|>


<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
$(function() {

  ManagedRepository=function(id,name,layout,indexDirectory,location,snapshots,releases,blockRedeployments,cronExpression,
                             scanned,daysOlder,retentionCount,deleteReleasedSnapshots,stageRepoNeeded){

    //private String id;
    this.id=ko.observable(id);

    //private String name;
    this.name=ko.observable(name);

    //private String layout = "default";
    this.layout=ko.observable(layout);

    //private String indexDirectory;
    this.indexDirectory=ko.observable(indexDirectory);

    //private String location;
    this.location=ko.observable(location);

    //private String cronExpression = "0 0 * * * ?";
    this.cronExpression=ko.observable(cronExpression);

    //private ManagedRepository stagingRepository;

    //private int daysOlder = 100;
    this.daysOlder=ko.observable(daysOlder);

    //private int retentionCount = 2;
    this.retentionCount=ko.observable(retentionCount);

    //private boolean scanned = false;
    this.scanned=ko.observable(scanned);

    //private boolean deleteReleasedSnapshots;
    this.deleteReleasedSnapshots=ko.observable(deleteReleasedSnapshots);

    //private boolean stageRepoNeeded;
    this.stageRepoNeeded=ko.observable(stageRepoNeeded);

    //private boolean snapshots = false;
    this.snapshots=ko.observable(snapshots);

    //private boolean releases = true;
    this.releases=ko.observable(releases);

    //private boolean blockRedeployments = false;
    this.blockRedeployments=ko.observable(blockRedeployments);

  }

  ManagedRepositoryViewModel=function(managedRepository, update, managedRepositoriesViewModel){
    this.managedRepository=managedRepository;
    this.managedRepositoriesViewModel = managedRepositoriesViewModel;
    this.update = update;
    var self = this;
    save=function(){
      var valid = $("#main-content #managed-repository-edit-form").valid();
      if (valid==false) {
          return;
      }
      $.log("save:"+this.managedRepository.name());
      clearUserMessages();
      if (this.update){
        $.ajax("restServices/archivaServices/managedRepositoriesService/updateManagedRepository",
          {
            type: "POST",
            contentType: 'application/json',
            data: "{\"managedRepository\": " +  ko.toJSON(this.managedRepository)+"}",
            dataType: 'json',
              success: function(data) {
                displaySuccessMessage($.i18n.prop('managedrepository.updated'));
                activateManagedRepositoriesGridTab();
              },
              error: function(data) {
                var res = $.parseJSON(data.responseText);
                displayRestError(res);
              }
          }
        );
      } else {
        $.log("add managedRepo");

        $.ajax("restServices/archivaServices/managedRepositoriesService/addManagedRepository",
          {
            type: "POST",
            contentType: 'application/json',
            data: "{\"managedRepository\": " + ko.toJSON(self.managedRepository)+"}",
            dataType: 'json',
            success: function(data) {
              //var repo = mapManagedRepository(data);
              //$.log("data:"+data.responseText);
              self.managedRepositoriesViewModel.managedRepositories.push(self.managedRepository);
              displaySuccessMessage($.i18n.prop('managedrepository.added'));
              activateManagedRepositoriesGridTab();
            },
            error: function(data) {
              var res = $.parseJSON(data.responseText);
              displayRestError(res);
            }
          }
        );
      }
    }

    displayGrid=function(){
      activateManagedRepositoriesGridTab();
    }

  }

  activateManagedRepositoryFormValidation=function(){
    $("#main-content #managed-repository-edit-form").validate({
      rules: {
        daysOlder : {
          digits: true
        },
        retentionCount : {
          digits: true
        }
      },
      showErrors: function(validator, errorMap, errorList) {
        customShowError(validator,errorMap,errorMap);
      }
    });
  }

  ManagedRepositoriesViewModel=function(){
    this.managedRepositories=ko.observableArray([]);

    this.gridViewModel = null;
    var self = this;

    editManagedRepository=function(managedRepository){
      var viewModel = new ManagedRepositoryViewModel(managedRepository,true,self);
      ko.applyBindings(viewModel,$("#main-content #managed-repository-edit").get(0));
      activateManagedRepositoryEditTab();
      $("#managed-repository-edit-li a").html($.i18n.prop('edit'));
      activateManagedRepositoryFormValidation();
    }

    removeManagedRepository=function(managedRepository){
      clearUserMessages();
      openDialogConfirm(
          function(){
            $("#dialog-confirm-modal #modal-login-footer").append(smallSpinnerImg());
            var url = "restServices/archivaServices/managedRepositoriesService/deleteManagedRepository?";
            url += "repositoryId="+encodeURIComponent(managedRepository.id());

            var checked = $("#managedrepository-deletecontent").get(0).checked;

            url += "&deleteContent="+(checked==true?"true":"false");
            $.ajax(url,
              {
                type: "GET",
                  success: function(data) {
                    self.managedRepositories.remove(managedRepository);
                    displaySuccessMessage($.i18n.prop("managedrepository.deleted",managedRepository.name()));

                  },
                  error: function(data) {
                    var res = $.parseJSON(data.responseText);
                    displayRestError(res);
                  },
                  complete: function(){
                    removeSmallSpinnerImg();
                    closeDialogConfirm();
                  }
              }
            );

          },
          $.i18n.prop("ok"),
          $.i18n.prop("cancel"),
          $.i18n.prop("managedrepository.delete.confirm",managedRepository.name()),
          $("#managed-repository-delete-warning-tmpl").tmpl(managedRepository));
    }

  }

  displayRepositoriesGrid=function(){
    clearUserMessages();
    $("#main-content").html(mediumSpinnerImg());
    $("#main-content").html($("#repositoriesMain").tmpl());
    $("#main-content #repositories-tabs").tabs();

    $("#main-content #managed-repositories-content").append(mediumSpinnerImg());
    $("#main-content #remote-repositories-content").append(mediumSpinnerImg());

    var managedRepositoriesViewModel = new ManagedRepositoriesViewModel();

    $.ajax("restServices/archivaServices/managedRepositoriesService/getManagedRepositories", {
        type: "GET",
        dataType: 'json',
        success: function(data) {
          managedRepositoriesViewModel.managedRepositories(mapManagedRepositories(data));
          managedRepositoriesViewModel.gridViewModel = new ko.simpleGrid.viewModel({
            data: managedRepositoriesViewModel.managedRepositories,
            columns: [
              {
                headerText: $.i18n.prop('identifier'),
                rowText: "id"
              },
              {
                headerText: $.i18n.prop('name'),
                rowText: "name"
              },
              {
                headerText: $.i18n.prop('type'),
                rowText: "layout",
                // FIXME i18n
                title: "Repository type (default is Maven 2)"
              }
            ],
            pageSize: 10
          });
          ko.applyBindings(managedRepositoriesViewModel,$("#main-content #managed-repositories-table").get(0));
          $("#main-content #managed-repositories-pills").pills();
          $("#managed-repositories-view").addClass("active");
          removeMediumSpinnerImg("#main-content #managed-repositories-content");
          $("#main-content #managed-repositories-table [title]").twipsy();
          activateManagedRepositoriesGridTab();
        }
      }
    );

    $("#main-content #managed-repositories-pills").bind('change', function (e) {
      if ($(e.target).attr("href")=="#managed-repository-edit") {
        var viewModel = new ManagedRepositoryViewModel(new ManagedRepository(),false,managedRepositoriesViewModel);
        ko.applyBindings(viewModel,$("#main-content #managed-repository-edit").get(0));
        activateManagedRepositoryFormValidation();
      }
      if ($(e.target).attr("href")=="#managed-repositories-view") {
        $("#main-content #managed-repository-edit-li a").html($.i18n.prop("add"));
      }

    });

  }

  mapManagedRepositories=function(data){
    var mappedManagedRepositories = $.map(data.managedRepository, function(item) {
      return mapManagedRepository(item);
    });
    return mappedManagedRepositories;
  }
  mapManagedRepository=function(data){

    return new ManagedRepository(data.id,data.name,data.layout,data.indexDirectory,data.location,data.snapshots,data.releases,
                                 data.blockRedeployments,data.cronExpression,
                                 data.scanned,data.daysOlder,data.retentionCount,data.deleteReleasedSnapshots,data.stageRepoNeeded);
  }

  activateManagedRepositoriesGridTab=function(){
    $("#main-content #managed-repository-edit-li").removeClass("active");
    $("#main-content #managed-repository-edit").removeClass("active");
    // activate roles grid tab
    $("#main-content #managed-repositories-view-li").addClass("active");
    $("#main-content #managed-repositories-view").addClass("active");
    $("#main-content #managed-repository-edit-li a").html($.i18n.prop("add"));
  }

  activateManagedRepositoryEditTab=function(){
    $("#main-content #managed-repositories-view-li").removeClass("active");
    $("#main-content #managed-repositories-view").removeClass("active");
    // activate role edit tab
    $("#main-content #managed-repository-edit-li").addClass("active");
    $("#main-content #managed-repository-edit").addClass("active");
  }

});<commit_msg>more validation rules for managed repository form<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */
$(function() {

  ManagedRepository=function(id,name,layout,indexDirectory,location,snapshots,releases,blockRedeployments,cronExpression,
                             scanned,daysOlder,retentionCount,deleteReleasedSnapshots,stageRepoNeeded){

    //private String id;
    this.id=ko.observable(id);

    //private String name;
    this.name=ko.observable(name);

    //private String layout = "default";
    this.layout=ko.observable(layout);

    //private String indexDirectory;
    this.indexDirectory=ko.observable(indexDirectory);

    //private String location;
    this.location=ko.observable(location);

    //private String cronExpression = "0 0 * * * ?";
    this.cronExpression=ko.observable(cronExpression);

    //private ManagedRepository stagingRepository;

    //private int daysOlder = 100;
    this.daysOlder=ko.observable(daysOlder);

    //private int retentionCount = 2;
    this.retentionCount=ko.observable(retentionCount);

    //private boolean scanned = false;
    this.scanned=ko.observable(scanned);

    //private boolean deleteReleasedSnapshots;
    this.deleteReleasedSnapshots=ko.observable(deleteReleasedSnapshots);

    //private boolean stageRepoNeeded;
    this.stageRepoNeeded=ko.observable(stageRepoNeeded);

    //private boolean snapshots = false;
    this.snapshots=ko.observable(snapshots);

    //private boolean releases = true;
    this.releases=ko.observable(releases);

    //private boolean blockRedeployments = false;
    this.blockRedeployments=ko.observable(blockRedeployments);

  }

  ManagedRepositoryViewModel=function(managedRepository, update, managedRepositoriesViewModel){
    this.managedRepository=managedRepository;
    this.managedRepositoriesViewModel = managedRepositoriesViewModel;
    this.update = update;
    var self = this;
    save=function(){
      var valid = $("#main-content #managed-repository-edit-form").valid();
      if (valid==false) {
          return;
      }
      $.log("save:"+this.managedRepository.name());
      clearUserMessages();
      if (this.update){
        $.ajax("restServices/archivaServices/managedRepositoriesService/updateManagedRepository",
          {
            type: "POST",
            contentType: 'application/json',
            data: "{\"managedRepository\": " +  ko.toJSON(this.managedRepository)+"}",
            dataType: 'json',
              success: function(data) {
                displaySuccessMessage($.i18n.prop('managedrepository.updated'));
                activateManagedRepositoriesGridTab();
              },
              error: function(data) {
                var res = $.parseJSON(data.responseText);
                displayRestError(res);
              }
          }
        );
      } else {
        $.log("add managedRepo");

        $.ajax("restServices/archivaServices/managedRepositoriesService/addManagedRepository",
          {
            type: "POST",
            contentType: 'application/json',
            data: "{\"managedRepository\": " + ko.toJSON(self.managedRepository)+"}",
            dataType: 'json',
            success: function(data) {
              //var repo = mapManagedRepository(data);
              //$.log("data:"+data.responseText);
              self.managedRepositoriesViewModel.managedRepositories.push(self.managedRepository);
              displaySuccessMessage($.i18n.prop('managedrepository.added'));
              activateManagedRepositoriesGridTab();
            },
            error: function(data) {
              var res = $.parseJSON(data.responseText);
              displayRestError(res);
            }
          }
        );
      }
    }

    displayGrid=function(){
      activateManagedRepositoriesGridTab();
    }

  }

  activateManagedRepositoryFormValidation=function(){
    $("#main-content #managed-repository-edit-form").validate({
      rules: {
        daysOlder : {
          digits: true,
          min: 1
        },
        retentionCount : {
          digits: true,
          min: 1,
          max: 100
        }
      },
      showErrors: function(validator, errorMap, errorList) {
        customShowError(validator,errorMap,errorMap);
      }
    });
  }

  ManagedRepositoriesViewModel=function(){
    this.managedRepositories=ko.observableArray([]);

    this.gridViewModel = null;
    var self = this;

    editManagedRepository=function(managedRepository){
      var viewModel = new ManagedRepositoryViewModel(managedRepository,true,self);
      ko.applyBindings(viewModel,$("#main-content #managed-repository-edit").get(0));
      activateManagedRepositoryEditTab();
      $("#managed-repository-edit-li a").html($.i18n.prop('edit'));
      activateManagedRepositoryFormValidation();
    }

    removeManagedRepository=function(managedRepository){
      clearUserMessages();
      openDialogConfirm(
          function(){
            $("#dialog-confirm-modal #modal-login-footer").append(smallSpinnerImg());
            var url = "restServices/archivaServices/managedRepositoriesService/deleteManagedRepository?";
            url += "repositoryId="+encodeURIComponent(managedRepository.id());

            var checked = $("#managedrepository-deletecontent").get(0).checked;

            url += "&deleteContent="+(checked==true?"true":"false");
            $.ajax(url,
              {
                type: "GET",
                  success: function(data) {
                    self.managedRepositories.remove(managedRepository);
                    displaySuccessMessage($.i18n.prop("managedrepository.deleted",managedRepository.name()));

                  },
                  error: function(data) {
                    var res = $.parseJSON(data.responseText);
                    displayRestError(res);
                  },
                  complete: function(){
                    removeSmallSpinnerImg();
                    closeDialogConfirm();
                  }
              }
            );

          },
          $.i18n.prop("ok"),
          $.i18n.prop("cancel"),
          $.i18n.prop("managedrepository.delete.confirm",managedRepository.name()),
          $("#managed-repository-delete-warning-tmpl").tmpl(managedRepository));
    }

  }

  displayRepositoriesGrid=function(){
    clearUserMessages();
    $("#main-content").html(mediumSpinnerImg());
    $("#main-content").html($("#repositoriesMain").tmpl());
    $("#main-content #repositories-tabs").tabs();

    $("#main-content #managed-repositories-content").append(mediumSpinnerImg());
    $("#main-content #remote-repositories-content").append(mediumSpinnerImg());

    var managedRepositoriesViewModel = new ManagedRepositoriesViewModel();

    $.ajax("restServices/archivaServices/managedRepositoriesService/getManagedRepositories", {
        type: "GET",
        dataType: 'json',
        success: function(data) {
          managedRepositoriesViewModel.managedRepositories(mapManagedRepositories(data));
          managedRepositoriesViewModel.gridViewModel = new ko.simpleGrid.viewModel({
            data: managedRepositoriesViewModel.managedRepositories,
            columns: [
              {
                headerText: $.i18n.prop('identifier'),
                rowText: "id"
              },
              {
                headerText: $.i18n.prop('name'),
                rowText: "name"
              },
              {
                headerText: $.i18n.prop('type'),
                rowText: "layout",
                // FIXME i18n
                title: "Repository type (default is Maven 2)"
              }
            ],
            pageSize: 10
          });
          ko.applyBindings(managedRepositoriesViewModel,$("#main-content #managed-repositories-table").get(0));
          $("#main-content #managed-repositories-pills").pills();
          $("#managed-repositories-view").addClass("active");
          removeMediumSpinnerImg("#main-content #managed-repositories-content");
          $("#main-content #managed-repositories-table [title]").twipsy();
          activateManagedRepositoriesGridTab();
        }
      }
    );

    $("#main-content #managed-repositories-pills").bind('change', function (e) {
      if ($(e.target).attr("href")=="#managed-repository-edit") {
        var viewModel = new ManagedRepositoryViewModel(new ManagedRepository(),false,managedRepositoriesViewModel);
        ko.applyBindings(viewModel,$("#main-content #managed-repository-edit").get(0));
        activateManagedRepositoryFormValidation();
      }
      if ($(e.target).attr("href")=="#managed-repositories-view") {
        $("#main-content #managed-repository-edit-li a").html($.i18n.prop("add"));
      }

    });

  }

  mapManagedRepositories=function(data){
    var mappedManagedRepositories = $.map(data.managedRepository, function(item) {
      return mapManagedRepository(item);
    });
    return mappedManagedRepositories;
  }
  mapManagedRepository=function(data){

    return new ManagedRepository(data.id,data.name,data.layout,data.indexDirectory,data.location,data.snapshots,data.releases,
                                 data.blockRedeployments,data.cronExpression,
                                 data.scanned,data.daysOlder,data.retentionCount,data.deleteReleasedSnapshots,data.stageRepoNeeded);
  }

  activateManagedRepositoriesGridTab=function(){
    $("#main-content #managed-repository-edit-li").removeClass("active");
    $("#main-content #managed-repository-edit").removeClass("active");
    // activate roles grid tab
    $("#main-content #managed-repositories-view-li").addClass("active");
    $("#main-content #managed-repositories-view").addClass("active");
    $("#main-content #managed-repository-edit-li a").html($.i18n.prop("add"));
  }

  activateManagedRepositoryEditTab=function(){
    $("#main-content #managed-repositories-view-li").removeClass("active");
    $("#main-content #managed-repositories-view").removeClass("active");
    // activate role edit tab
    $("#main-content #managed-repository-edit-li").addClass("active");
    $("#main-content #managed-repository-edit").addClass("active");
  }

});<|endoftext|>


<commit_before>// (C) Copyright 2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';

const CLASS_ROOT = "value";

export default class Value extends Component {

  render () {
    const classes = [CLASS_ROOT];
    if (this.props.size) {
      classes.push(`${CLASS_ROOT}--${this.props.size}`);
    }
    if (this.props.align) {
      classes.push(`${CLASS_ROOT}--align-${this.props.align}`);
    }
    if (this.props.onClick) {
      classes.push(`${CLASS_ROOT}--interactive`);
    }
    if (this.props.colorIndex) {
      classes.push(`color-index-${this.props.colorIndex}`);
    }
    if (this.props.className) {
      classes.push(this.props.className);
    }

    let units;
    if (this.props.units) {
      units = (
        <span className={`${CLASS_ROOT}__units`}>
          {this.props.units}

      );
    }

    let label;
    if (this.props.label) {
      label = (
        <span className={`${CLASS_ROOT}__label`}>
          {this.props.label}

      );
    }

    return (

        <div className={`${CLASS_ROOT}__annotated`}>
          {this.props.icon}
          <span className={`${CLASS_ROOT}__value`}>
            {this.props.value}

          {units}
          {this.props.trendIcon}

        {label}

    );
  }

}

Value.propTypes = {
  align: PropTypes.oneOf(['start', 'center', 'end']),
  colorIndex: PropTypes.string,
  icon: PropTypes.node,
  label: PropTypes.string,
  onClick: PropTypes.func,
  size: PropTypes.oneOf(['small', 'medium', 'large', 'xlarge']),
  trendIcon: PropTypes.node,
  value: PropTypes.number.isRequired,
  units: PropTypes.string
};

Value.defaultProps = {
  align: 'center'
};
<commit_msg>Allow string value in Value.<commit_after>// (C) Copyright 2016 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';

const CLASS_ROOT = "value";

export default class Value extends Component {

  render () {
    const classes = [CLASS_ROOT];
    if (this.props.size) {
      classes.push(`${CLASS_ROOT}--${this.props.size}`);
    }
    if (this.props.align) {
      classes.push(`${CLASS_ROOT}--align-${this.props.align}`);
    }
    if (this.props.onClick) {
      classes.push(`${CLASS_ROOT}--interactive`);
    }
    if (this.props.colorIndex) {
      classes.push(`color-index-${this.props.colorIndex}`);
    }
    if (this.props.className) {
      classes.push(this.props.className);
    }

    let units;
    if (this.props.units) {
      units = (
        <span className={`${CLASS_ROOT}__units`}>
          {this.props.units}

      );
    }

    let label;
    if (this.props.label) {
      label = (
        <span className={`${CLASS_ROOT}__label`}>
          {this.props.label}

      );
    }

    return (

        <div className={`${CLASS_ROOT}__annotated`}>
          {this.props.icon}
          <span className={`${CLASS_ROOT}__value`}>
            {this.props.value}

          {units}
          {this.props.trendIcon}

        {label}

    );
  }

}

Value.propTypes = {
  align: PropTypes.oneOf(['start', 'center', 'end']),
  colorIndex: PropTypes.string,
  icon: PropTypes.node,
  label: PropTypes.string,
  onClick: PropTypes.func,
  size: PropTypes.oneOf(['small', 'medium', 'large', 'xlarge']),
  trendIcon: PropTypes.node,
  value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
  units: PropTypes.string
};

Value.defaultProps = {
  align: 'center'
};
<|endoftext|>


<commit_before>goog.provide('lime.audio.AudioMap');

goog.require('goog.events');
goog.require('lime.userAgent');
goog.require('jukebox.Manager');
goog.require('jukebox.Player');

/**
 * AudioMap object
 * @constructor
 * @param {object} config Configuration object.
 */
lime.audio.AudioMap = function(config) {
	if (config.data && goog.isFunction(config.data)) {
		config = config.data();
	}
	this.config = config;

	if (lime.userAgent.IOS || lime.userAgent.WINPHONE) {
		goog.events.listenOnce(window, lime.userAgent.SUPPORTS_TOUCH ? 'touchstart' : 'mousedown', this._initPlayer, true, this);
	}
	else {
		this._initPlayer();
	}
};

lime.audio.AudioMap.prototype._initPlayer = function() {
	this.player = new jukebox.Player(this.config);
}

/**
 * Start playing the audio
 * @param {string} sprite Sprite name to play.
 */
lime.audio.AudioMap.prototype.play = function(sprite) {
	if (this.player && this.config.spritemap[sprite]) {
		this.player.play(sprite);
	}
};

/**
 * Stop playing the audio
 */
lime.audio.AudioMap.prototype.stop = function() {
	if (this.player) {
		this.player.pause();
	}
};

<commit_msg>Fix tabs<commit_after>goog.provide('lime.audio.AudioMap');

goog.require('goog.events');
goog.require('lime.userAgent');
goog.require('jukebox.Manager');
goog.require('jukebox.Player');

/**
 * AudioMap object
 * @constructor
 * @param {object} config Configuration object.
 */
lime.audio.AudioMap = function(config) {
    if (config.data && goog.isFunction(config.data)) {
        config = config.data();
    }
    this.config = config;

    if (lime.userAgent.IOS || lime.userAgent.WINPHONE) {
        goog.events.listenOnce(window, lime.userAgent.SUPPORTS_TOUCH ? 'touchstart' : 'mousedown', this._initPlayer, true, this);
    }
    else {
        this._initPlayer();
    }
};

lime.audio.AudioMap.prototype._initPlayer = function() {
    this.player = new jukebox.Player(this.config);
}

/**
 * Start playing the audio
 * @param {string} sprite Sprite name to play.
 */
lime.audio.AudioMap.prototype.play = function(sprite) {
    if (this.player && this.config.spritemap[sprite]) {
        this.player.play(sprite);
    }
};

/**
 * Stop playing the audio
 */
lime.audio.AudioMap.prototype.stop = function() {
    if (this.player) {
        this.player.pause();
    }
};

<|endoftext|>