<commit_before>import L from 'leaflet';

import { getEditorContent } from '../editor/editor';
import { map } from '../map/map';
import { getScreenshotData } from '../map/screenshot';
import { createThumbnail } from '../tools/thumbnail';

import store from '../store';
import config from '../config';

const THUMBNAIL_WIDTH = 144;
const THUMBNAIL_HEIGHT = 81;

// We only need the user-id for the API (plus, I assume, cookie credentials).
// This is for retrieving just the user ID from Redux store. Maybe this goes
// elsewhere eventually.
function getUserId() {
    return store.getState().user.id;
}

// wrap fetch to make it work both locally and stuff
function makeMapzenAPIRequest(path, options = {}) {
    const apiPath = config.MAPZEN_API.SCENE_API_PATH;
    const mergeOptions = {
        ...options,
        credentials: 'same-origin',
    };

    let baseUrl;

    // use hostname to determine environment
    // (should we use process.env.NODE_ENV for this?)
    switch (window.location.hostname) {
        case 'localhost':
            baseUrl = config.MAPZEN_API.ORIGIN.DEVELOPMENT;
            mergeOptions.credentials = 'include';
            break;
        case 'dev.mapzen.com':
            baseUrl = config.MAPZEN_API.ORIGIN.STAGING;
            break;
        default:
            baseUrl = config.MAPZEN_API.ORIGIN.PRODUCTION;
    }

    // Return wrapped fetch. This also wraps the part that checks if response
    // is OK and throws an error or returns JSON.
    return window.fetch(`${baseUrl}${apiPath}${path}`, mergeOptions)
        .then(response => {
            if (!response.ok) {
                throw new Error(response.status);
            }

            return response.json();
        });
}

/**
 * Create a thumbnail image of the current map scene as a Data-URI (which is
 * currently the format accepted by the Mapzen scene API).
 *
 * @returns {Promise} - fulfilled with the screenshot as a Data-URI.
 */
function makeThumbnail() {
    // Grab a screenshot from the map and convert it to a thumbnail at a fixed
    // dimension. This makes file sizes and layout more predictable.
    return getScreenshotData()
        // At this size, thumbnail image should clock in at around ~90kb
        // to ~120kb (unoptimized, but that's the limitations of our
        // thumbnail function)
        .then(screenshot => createThumbnail(screenshot.blob, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, true, false));
}

/**
 * Creates additional metadata information about the current scene.
 *
 * @params {Object} data - data passed to saveToMapzenUserAccount()
 * @returns {Object} metadata - additional metadata for the Scene API
 */
function addMetadata(data) {
    // Add some additional view information to the metadata.
    const mapLabel = store.getState().map.label;
    const metadata = Object.assign({}, data, {
        view_label: mapLabel || '',
        view_lat: map.getCenter().lat,
        view_lng: map.getCenter().lng,
        view_zoom: map.getZoom(),
        versions_leaflet: L.version,
        versions_tangram: window.Tangram.version,
    });

    return metadata;
}

/**
 * Retrieve scene list from Mapzen scene API. Returns a Promise, resolved with
 * its contents in the form of an Array. This array is empty if there are no
 * scenes saved.
 *
 * @returns {Promise} - resolved with an array of saved scene data (or empty
 *          array if nothing is saved yet)
 */
export function fetchSceneList() {
    const userId = getUserId();

    return makeMapzenAPIRequest(userId)
        .then(scenes => {
            // API gives an object with error property if an error occurred.
            if (scenes.error) {
                throw new Error(scenes.error);
            }

            // Returns with array of scenes (or empty array if no scenes).
            return scenes;
        });
}

export function saveToMapzenUserAccount(data) {
    const userId = getUserId();

    // Add other data - here, or provided upstream?
    const metadata = addMetadata(data);

    // This is a single YAML file for now - whatever's in the current editor.
    // TODO: get only root scene file
    const content = getEditorContent();

    // POST a new scene.
    return makeThumbnail()
        .then(screenshotData => {
            // Add thumbnail as data-URI to payload
            metadata.thumbnail = screenshotData;

            const requestOptions = {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(metadata),
            };

            return makeMapzenAPIRequest(`${userId}`, requestOptions);
        })
        // The returned `sceneData` will contain the `id` and
        // `resources_url` needed to POST each of our scene resources.

        // temp: to deal with this not needing url concatenation
        .then(sceneData => window.fetch(sceneData.entrypoint_url, {
            method: 'PUT',
            headers: { 'Content-Type': 'text/x-yaml' },
            body: content,
            credentials: 'include',
        })
        .then(response => {
            console.log(response);
            // Return original scene data to success handler.
            return sceneData;
        }));
}
<commit_msg>Handle errors on PUT<commit_after>import L from 'leaflet';

import { getEditorContent } from '../editor/editor';
import { map } from '../map/map';
import { getScreenshotData } from '../map/screenshot';
import { createThumbnail } from '../tools/thumbnail';

import store from '../store';
import config from '../config';

const THUMBNAIL_WIDTH = 144;
const THUMBNAIL_HEIGHT = 81;

// We only need the user-id for the API (plus, I assume, cookie credentials).
// This is for retrieving just the user ID from Redux store. Maybe this goes
// elsewhere eventually.
function getUserId() {
    return store.getState().user.id;
}

// wrap fetch to make it work both locally and stuff
function makeMapzenAPIRequest(path, options = {}) {
    const apiPath = config.MAPZEN_API.SCENE_API_PATH;
    const mergeOptions = {
        ...options,
        credentials: 'same-origin',
    };

    let baseUrl;

    // use hostname to determine environment
    // (should we use process.env.NODE_ENV for this?)
    switch (window.location.hostname) {
        case 'localhost':
            baseUrl = config.MAPZEN_API.ORIGIN.DEVELOPMENT;
            mergeOptions.credentials = 'include';
            break;
        case 'dev.mapzen.com':
            baseUrl = config.MAPZEN_API.ORIGIN.STAGING;
            break;
        default:
            baseUrl = config.MAPZEN_API.ORIGIN.PRODUCTION;
    }

    // Return wrapped fetch. This also wraps the part that checks if response
    // is OK and throws an error or returns JSON.
    return window.fetch(`${baseUrl}${apiPath}${path}`, mergeOptions)
        .then(response => {
            if (!response.ok) {
                throw new Error(response.status);
            }

            return response.json();
        });
}

/**
 * Create a thumbnail image of the current map scene as a Data-URI (which is
 * currently the format accepted by the Mapzen scene API).
 *
 * @returns {Promise} - fulfilled with the screenshot as a Data-URI.
 */
function makeThumbnail() {
    // Grab a screenshot from the map and convert it to a thumbnail at a fixed
    // dimension. This makes file sizes and layout more predictable.
    return getScreenshotData()
        // At this size, thumbnail image should clock in at around ~90kb
        // to ~120kb (unoptimized, but that's the limitations of our
        // thumbnail function)
        .then(screenshot => createThumbnail(screenshot.blob, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, true, false));
}

/**
 * Creates additional metadata information about the current scene.
 *
 * @params {Object} data - data passed to saveToMapzenUserAccount()
 * @returns {Object} metadata - additional metadata for the Scene API
 */
function addMetadata(data) {
    // Add some additional view information to the metadata.
    const mapLabel = store.getState().map.label;
    const metadata = Object.assign({}, data, {
        view_label: mapLabel || '',
        view_lat: map.getCenter().lat,
        view_lng: map.getCenter().lng,
        view_zoom: map.getZoom(),
        versions_leaflet: L.version,
        versions_tangram: window.Tangram.version,
    });

    return metadata;
}

/**
 * Retrieve scene list from Mapzen scene API. Returns a Promise, resolved with
 * its contents in the form of an Array. This array is empty if there are no
 * scenes saved.
 *
 * @returns {Promise} - resolved with an array of saved scene data (or empty
 *          array if nothing is saved yet)
 */
export function fetchSceneList() {
    const userId = getUserId();

    return makeMapzenAPIRequest(userId)
        .then(scenes => {
            // API gives an object with error property if an error occurred.
            if (scenes.error) {
                throw new Error(scenes.error);
            }

            // Returns with array of scenes (or empty array if no scenes).
            return scenes;
        });
}

export function saveToMapzenUserAccount(data) {
    const userId = getUserId();

    // Add other data - here, or provided upstream?
    const metadata = addMetadata(data);

    // This is a single YAML file for now - whatever's in the current editor.
    // TODO: get only root scene file
    const content = getEditorContent();

    // POST a new scene.
    return makeThumbnail()
        .then(screenshotData => {
            // Add thumbnail as data-URI to payload
            metadata.thumbnail = screenshotData;

            const requestOptions = {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(metadata),
            };

            return makeMapzenAPIRequest(`${userId}`, requestOptions);
        })
        // The returned `sceneData` will contain the `id` and
        // `resources_url` needed to POST each of our scene resources.

        // temp: to deal with this not needing url concatenation
        .then(sceneData => window.fetch(sceneData.entrypoint_url, {
            method: 'PUT',
            headers: { 'Content-Type': 'text/x-yaml' },
            body: content,
            credentials: 'include',
        })
        .then(response => {
            // There may be errors
            if (!response.ok) {
                const message = `There was a problem saving your scene. Error code ${response.status}`;
                throw new Error(message);
            }

            // Return original scene data to success handler.
            return sceneData;
        }));
}
<|endoftext|>


<commit_before>/**
 * Truncate string with options for clean break and ellipses
 * @param {string} text to trim
 * @param {number} [chars=100] number of characters to cut at
 * @param {boolean} [clean=true] break on space?
 * @param {boolean} [ellipses=false] add ...

 * @returns {string} truncated text
 */
function truncate({ text, chars = 100, clean = true, ellipses = false }) {
	const a = text.substring(0, chars);
	const end = clean ? sub.lastIndexOf(' ') : a.length;
	const b = sub.substring(0, end);
	const e = ellipses ? '...' : '';
	return `${b}${e}`;
}

export default truncate;
<commit_msg>fix truncate<commit_after>/**
 * Truncate string with options for clean break and ellipses
 * @param {string} text to trim
 * @param {number} [chars=100] number of characters to cut at
 * @param {boolean} [clean=true] break on space?
 * @param {boolean} [ellipses=false] add ...

 * @returns {string} truncated text
 */
function truncate({ text, chars = 100, clean = true, ellipses = false }) {
	const a = text.substring(0, chars);
	const end = clean ? a.lastIndexOf(' ') : a.length;
	const b = a.substring(0, end);
	const e = ellipses ? '...' : '';
	return `${b}${e}`;
}

export default truncate;
<|endoftext|>


<commit_before>
TreeView = Backbone.View.extend({

    /**
     * Function to determine the width of scroll bars.
     */
    getScrollbarWidth: function() {
        var outer = document.createElement("div");
        outer.style.visibility = "hidden";
        outer.style.width = "100px";
        document.body.appendChild(outer);

        var widthNoScroll = outer.offsetWidth;
        // force scrollbars
        outer.style.overflow = "scroll";

        // add innerdiv
        var inner = document.createElement("div");
        inner.style.width = "100%";
        outer.appendChild(inner);

        var widthWithScroll = inner.offsetWidth;

        // remove divs
        outer.parentNode.removeChild(outer);

        return widthNoScroll - widthWithScroll;
    },

    initialize: function() {

        // Layout constants
        this.scrollbarWidth = this.getScrollbarWidth();
        this.treeLayoutMarginX = 120;
        this.treeLayoutMarginY = 10;

        // Enable debug mode
        this.debug = (this.options.debug == true);

        // Get drawing context
        var $main = this.$el.find('.main');
        var context = $main.get(0).getContext('2d');
        if (!context) {
            return;
        }

        $main.mousedown(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        $(document).mousemove(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        $(document).mouseup(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        this.renderer = new PuzzleStateRenderer(context);

        // Don't display the tree thumbnail
        this.renderThumbnail = false;

        // Initialise the layout algorithm
        this.layoutAlgorithm = new TreeLayout({
            nodeWidth: this.renderer.getExpectedWidth(),
            nodeHeight: this.renderer.getExpectedHeight()
        });

        // Respond to changes within the tree by recalculating the layout of
        // the tree, then rendering the result.
        this.model.getTree().on('change', this.layoutAndRenderTree, this);

        // When the user scrolls using the virtual viewport, the offset of the
        // the search tree should be updated using the scrollbar positions.
        this.$viewport = this.$el.find('#viewport');
        this.$viewport.scroll(_.bind(this.render, this));

        // Respond to the tree being reset by resetting the virtual viewport
        this.model.on('reset:tree', function() {
            this.$viewport
                .css('width', '0px')
                .css('height', '0px');
            },
            this
        );
    },

    layoutAndRenderTree: function() {
        // Position and draw the tree
        var tree = this.model.getTree();
        if (tree != null) {
            this.layoutAlgorithm.positionTree(tree.getRootNode());
            this.render();
        }
    },

    renderSubtree: function(context, node, coords, xOffset, yOffset, width, height) {

        var nodeAttributes = node.getAttributes();
        var stateColor = '#000';

        switch (node.getAttributes().kind) {
        case 'repeat':
            stateColor = '#F00';
            break;
        case 'goal':
            stateColor = '#0F0';
            break;
        default:
            break;
        }

        // Render this state
        var x = coords.x + xOffset;
        var y = coords.y + yOffset;
        var expectedWidth = this.renderer.getExpectedWidth();
        var expectedHeight = this.renderer.getExpectedHeight();
        if (x + expectedWidth >= 0 && x < width &&
            y + expectedHeight >= 0 && y < height) {
            this.renderer.renderState(node.getState(), stateColor, x, y);
        }

        // Recursively render child states
        var children = node.getChildren();
        for (childIndex in children) {

            // Get nodes for children coordinate
            var childCoords = this.layoutAlgorithm.getCoordinatesForNode(children[childIndex]);
            if (childCoords == null) {
                console.log('Warning: stopped drawing tree due to missing node coordinates');
                return;
            }

            childCoords.x = Math.round(childCoords.x);
            childCoords.y = Math.round(childCoords.y);

            // Draw a line connecting the parent node to its children
            context.beginPath();
            context.moveTo(
                coords.x + xOffset + expectedWidth / 2 + 0.5,
                coords.y + yOffset + expectedHeight + 1);
            context.lineTo(
                childCoords.x + xOffset + expectedWidth / 2 + 0.5,
                childCoords.y + yOffset);
            context.strokeStyle = '#000';
            context.stroke();

            this.renderSubtree(context, children[childIndex], childCoords, xOffset, yOffset, width, height);
        }
    },

    render: function() {

        var $main = this.$el.find('.main');
        var width = $main.attr('width');
        var height = $main.attr('height');
        if (width == 0 || height == 0) {
            return;
        }

        var context = $main.get(0).getContext('2d');
        if (!context) {
            return;
        }

        // Clear canvas
        context.clearRect(0, 0, width, height);

        // Center the tree in the viewport
        var boundingBox = this.layoutAlgorithm.getBoundingBox();
        if (boundingBox == null) {
            return;
        }

        var boundingWidth = boundingBox.right - boundingBox.left;
        var boundingHeight = boundingBox.bottom - boundingBox.top;

        var xOffset = Math.max(
            Math.round((width - boundingWidth) / 2 - boundingBox.left),
            this.treeLayoutMarginX - boundingBox.left
        ) - this.$viewport.scrollLeft();

        var yOffset = this.treeLayoutMarginY - boundingBox.top - this.$viewport.scrollTop();

        virtualDoc = {
            x: this.treeLayoutMarginX,
            y: this.treeLayoutMarginY,
            width: Math.max(width - this.treeLayoutMarginX * 2, boundingWidth),
            height: Math.max(height - this.treeLayoutMarginY * 2, boundingHeight)
        };

        if (this.debug) {
            context.strokeStyle = '#CCC';
            context.strokeRect(
                    Math.round(virtualDoc.x) + 0.5 - this.$viewport.scrollLeft(),
                    Math.round(virtualDoc.y) + 0.5 - this.$viewport.scrollTop(),
                    virtualDoc.width,
                    virtualDoc.height);
        }

        this.$el.find('#fakescrolldiv')
            .css('width', (virtualDoc.width + this.treeLayoutMarginX * 2) + "px")
            .css('height', (virtualDoc.height + this.treeLayoutMarginY * 2) + "px");

        // Position and draw the tree
        var tree = this.model.getTree();
        if (tree != null) {
            var rootNode = tree.getRootNode();
            if (rootNode != null) {

                // Get coordinates for root node
                var coords = this.layoutAlgorithm.getCoordinatesForNode(rootNode);
                coords.x = Math.round(coords.x);
                coords.y = Math.round(coords.y);

                this.renderSubtree(context, rootNode, coords, xOffset, yOffset, width, height);
            }
        }

        // Draw debugging rectangle
        if (this.debug) {
            context.strokeStyle = '#CCC';
            context.strokeRect(
                Math.round(boundingBox.left + xOffset) + 0.5,
                Math.round(boundingBox.top + yOffset) + 0.5,
                boundingBox.right - boundingBox.left,
                boundingBox.bottom - boundingBox.top);
        }
    },

    setSize: function(newWidth, newHeight) {

        // Resize container
        this.$el
            .css('width', newWidth)       // Canvas width
            .css('height', newHeight);    // Canvas height

        // Resize main canvas
        this.$el.find('.main')
            .attr('width', this.$el.innerWidth() - this.scrollbarWidth)
            .attr('height', this.$el.innerHeight() - this.scrollbarWidth);

        this.$el.find('#viewport')
            .css('overflow', 'scroll')
            .css('position', 'absolute')
            .css('width', newWidth)
            .css('height', newHeight);

        this.render();
    }
});<commit_msg>Change Tree View to use individual sizes for all four margins.<commit_after>
TreeView = Backbone.View.extend({

    /**
     * Function to determine the width of scroll bars.
     */
    getScrollbarWidth: function() {
        var outer = document.createElement("div");
        outer.style.visibility = "hidden";
        outer.style.width = "100px";
        document.body.appendChild(outer);

        var widthNoScroll = outer.offsetWidth;
        // force scrollbars
        outer.style.overflow = "scroll";

        // add innerdiv
        var inner = document.createElement("div");
        inner.style.width = "100%";
        outer.appendChild(inner);

        var widthWithScroll = inner.offsetWidth;

        // remove divs
        outer.parentNode.removeChild(outer);

        return widthNoScroll - widthWithScroll;
    },

    initialize: function() {

        // Layout constants
        this.scrollbarWidth = this.getScrollbarWidth();
        this.treeLayoutMarginLeft = 120;
        this.treeLayoutMarginRight = 120;
        this.treeLayoutMarginTop = 10;
        this.treeLayoutMarginBottom = 20;

        // Enable debug mode
        this.debug = (this.options.debug == true);

        // Get drawing context
        var $main = this.$el.find('.main');
        var context = $main.get(0).getContext('2d');
        if (!context) {
            return;
        }

        $main.mousedown(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        $(document).mousemove(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        $(document).mouseup(_.bind(function(event){
            // TODO: implement node selection
            event.preventDefault();
        }, this));

        this.renderer = new PuzzleStateRenderer(context);

        // Don't display the tree thumbnail
        this.renderThumbnail = false;

        // Initialise the layout algorithm
        this.layoutAlgorithm = new TreeLayout({
            nodeWidth: this.renderer.getExpectedWidth(),
            nodeHeight: this.renderer.getExpectedHeight()
        });

        // Respond to changes within the tree by recalculating the layout of
        // the tree, then rendering the result.
        this.model.getTree().on('change', this.layoutAndRenderTree, this);

        // When the user scrolls using the virtual viewport, the offset of the
        // the search tree should be updated using the scrollbar positions.
        this.$viewport = this.$el.find('#viewport');
        this.$viewport.scroll(_.bind(this.render, this));

        // Respond to the tree being reset by resetting the virtual viewport
        this.model.on('reset:tree', function() {
            this.$viewport
                .css('width', '0px')
                .css('height', '0px');
            },
            this
        );
    },

    layoutAndRenderTree: function() {
        // Position and draw the tree
        var tree = this.model.getTree();
        if (tree != null) {
            this.layoutAlgorithm.positionTree(tree.getRootNode());
            this.render();
        }
    },

    renderSubtree: function(context, node, coords, xOffset, yOffset, width, height) {

        var nodeAttributes = node.getAttributes();
        var stateColor = '#000';

        switch (node.getAttributes().kind) {
        case 'repeat':
            stateColor = '#F00';
            break;
        case 'goal':
            stateColor = '#0F0';
            break;
        default:
            break;
        }

        // Render this state
        var x = coords.x + xOffset;
        var y = coords.y + yOffset;
        var expectedWidth = this.renderer.getExpectedWidth();
        var expectedHeight = this.renderer.getExpectedHeight();
        if (x + expectedWidth >= 0 && x < width &&
            y + expectedHeight >= 0 && y < height) {
            this.renderer.renderState(node.getState(), stateColor, x, y);
        }

        // Recursively render child states
        var children = node.getChildren();
        for (childIndex in children) {

            // Get nodes for children coordinate
            var childCoords = this.layoutAlgorithm.getCoordinatesForNode(children[childIndex]);
            if (childCoords == null) {
                console.log('Warning: stopped drawing tree due to missing node coordinates');
                return;
            }

            childCoords.x = Math.round(childCoords.x);
            childCoords.y = Math.round(childCoords.y);

            // Draw a line connecting the parent node to its children
            context.beginPath();
            context.moveTo(
                coords.x + xOffset + expectedWidth / 2 + 0.5,
                coords.y + yOffset + expectedHeight + 1);
            context.lineTo(
                childCoords.x + xOffset + expectedWidth / 2 + 0.5,
                childCoords.y + yOffset);
            context.strokeStyle = '#000';
            context.stroke();

            this.renderSubtree(context, children[childIndex], childCoords, xOffset, yOffset, width, height);
        }
    },

    render: function() {

        var $main = this.$el.find('.main');
        var width = $main.attr('width');
        var height = $main.attr('height');
        if (width == 0 || height == 0) {
            return;
        }

        var context = $main.get(0).getContext('2d');
        if (!context) {
            return;
        }

        // Clear canvas
        context.clearRect(0, 0, width, height);

        // Center the tree in the viewport
        var boundingBox = this.layoutAlgorithm.getBoundingBox();
        if (boundingBox == null) {
            return;
        }

        var boundingWidth = boundingBox.right - boundingBox.left;
        var boundingHeight = boundingBox.bottom - boundingBox.top;

        var xOffset = Math.max(
            Math.round((width - boundingWidth) / 2 - boundingBox.left),
            this.treeLayoutMarginLeft - boundingBox.left
        ) - this.$viewport.scrollLeft();

        var yOffset = this.treeLayoutMarginTop - boundingBox.top - this.$viewport.scrollTop();

        virtualDoc = {
            x: this.treeLayoutMarginLeft,
            y: this.treeLayoutMarginTop,
            width: Math.max(width - this.treeLayoutMarginLeft + this.treeLayoutMarginRight, boundingWidth),
            height: Math.max(height - this.treeLayoutMarginTop + this.treeLayoutMarginBottom, boundingHeight)
        };

        if (this.debug) {
            context.strokeStyle = '#CCC';
            context.strokeRect(
                    Math.round(virtualDoc.x) + 0.5 - this.$viewport.scrollLeft(),
                    Math.round(virtualDoc.y) + 0.5 - this.$viewport.scrollTop(),
                    virtualDoc.width,
                    virtualDoc.height);
        }

        this.$el.find('#fakescrolldiv')
            .css('width', (virtualDoc.width + this.treeLayoutMarginLeft + this.treeLayoutMarginRight) + "px")
            .css('height', (virtualDoc.height + this.treeLayoutMarginTop + this.treeLayoutMarginBottom) + "px");

        // Position and draw the tree
        var tree = this.model.getTree();
        if (tree != null) {
            var rootNode = tree.getRootNode();
            if (rootNode != null) {

                // Get coordinates for root node
                var coords = this.layoutAlgorithm.getCoordinatesForNode(rootNode);
                coords.x = Math.round(coords.x);
                coords.y = Math.round(coords.y);

                this.renderSubtree(context, rootNode, coords, xOffset, yOffset, width, height);
            }
        }

        // Draw debugging rectangle
        if (this.debug) {
            context.strokeStyle = '#CCC';
            context.strokeRect(
                Math.round(boundingBox.left + xOffset) + 0.5,
                Math.round(boundingBox.top + yOffset) + 0.5,
                boundingBox.right - boundingBox.left,
                boundingBox.bottom - boundingBox.top);
        }
    },

    setSize: function(newWidth, newHeight) {

        // Resize container
        this.$el
            .css('width', newWidth)       // Canvas width
            .css('height', newHeight);    // Canvas height

        // Resize main canvas
        this.$el.find('.main')
            .attr('width', this.$el.innerWidth() - this.scrollbarWidth)
            .attr('height', this.$el.innerHeight() - this.scrollbarWidth);

        this.$el.find('#viewport')
            .css('overflow', 'scroll')
            .css('position', 'absolute')
            .css('width', newWidth)
            .css('height', newHeight);

        this.render();
    }
});<|endoftext|>


<commit_before>'use strict';

var FileUploaderComponent = BaseComponent.extend({
  update: function() {
    var myself = this,
      $ph = $("#" + this.htmlObject),
      root = this.rootFolder.charAt(this.rootFolder.length - 1) === "/" ? this.rootFolder.substring(0, this.rootFolder.length - 1) : this.rootFolder;

    var $fileSelect = $('
').addClass('fileSelect');
    $ph.empty();
    $ph.addClass('fileUploaderComponent');
    $ph.append($fileSelect);

    var $selectFile = $('
').addClass('uploadButton');
    var $fileObj = $('
').addClass('hide').addClass('fileTextRow');
    var $cancelButtonDiv = $('
').addClass('cancelButton');

    /*
     * initialize upload dialog container
     */
    var $uploadDialogContainer = $('

');
    var $uploadDialogHeader = $('
').addClass('popupHeader');
    var $uploadDialogBody = $('
').addClass('popupBody');
    var $uploadDialogFooter = $('
').addClass('popupFooter');

    // dialog header
    var $uploadPopupCloseButton = $('<button class="popupButton">×</button>');
    $uploadDialogHeader.append($uploadPopupCloseButton);
    var $uploadDialogHeaderTitle = $('
Uploading File
');
    $uploadDialogHeader.append($uploadDialogHeaderTitle);
    $uploadDialogContainer.append($uploadDialogHeader);

    // upload progress bar
    var $uploadProgressBarContainer = $('
').addClass('uploadBar');
    var $uploadProgressBar = $('

');

    $uploadProgressBarContainer.append($uploadProgressBar);
    $uploadDialogBody.append($uploadProgressBarContainer);

    // upload alert section
    var $uploadAlerts = $('

');
    $uploadDialogBody.append($uploadAlerts);

    $uploadDialogContainer.append($uploadDialogBody);

    var $uploadDialogDismissButton = $('<button type="button" class="popupButton" disabled>OK</button>');

    $uploadDialogFooter.append($uploadDialogDismissButton);

    $uploadDialogContainer.append($uploadDialogFooter);

    var $popupContentContainer = $('

');
    $popupContentContainer.append($uploadDialogContainer);
    $ph.append($popupContentContainer);

    // END: initialize upload dialog container

    var popup = new PopupComponent();
    popup.htmlObject = "uploaderPopupContainer";
    popup.draggable = false;
    popup.closeOnClickOutside = true;
    popup.popupClass = "uploadPopup";
    popup.popupOverlayClass = "fileUploaderComponentOverlay";
    popup.update();
    popup.ph.find(".close").remove();

    $uploadPopupCloseButton.click(function(event) {
      popup.hide();
    });
    $uploadDialogDismissButton.click(function(event) {
      popup.hide();
    });

    $fileSelect.append($selectFile)
      .append($fileObj).append($cancelButtonDiv);

    var $fileSelectTitle = $('
').text('Select File').addClass('select');
    var $uploadForm = $('<form action="' + Endpoints.getStore() + '" method="post" enctype="multipart/form-data">');

    var resetUploadForm = function() {
      $cancelButton.click();
    };

    var fileUploadedCallback = function(response) {
      if (response.result) {

        fileUploadProgressCallback(null, 0, 0, 100);

        // update alert
        $uploadAlerts.attr('class', 'alert success').text('File successfully uploaded');

        // activate upload dialog OK button
        $uploadDialogDismissButton.enable();

        //reset upload progress bar
        //fileUploadProgressCallback(null, 0, 0, 0);

        fireUploaded();
        resetUploadForm();
      } else {
        fileUploadErrorCallback()
      }
    };

    var fileUploadErrorCallback = function() {
      // update alert
      $uploadAlerts.attr('class', 'alert error').text('Error uploading file');
      // activate upload dialog OK button
      $uploadDialogDismissButton.enable();
    };

    var fileUploadProgressCallback = function(event, position, total, percent) {
      $uploadProgressBar.width(percent +"%");
    };

    var fileUploadBeforeSubmitCallback = function(arr, form, options) {
      // reset upload bar
      fileUploadProgressCallback(null, 0, 0, 0);

      // reset alerts
      $uploadAlerts.attr('class', 'hide').text('');

      // show upload dialog box
      popup.popup($ph);
      popup.ph.attr("style", "top: 15px; left:10px; right:10px; position:fixed;");
    };

    var fireUploaded = function(filename) {
      Dashboards.fireChange('uploadedFileParam', '');
    };

    // configure file upload form
    $uploadForm.ajaxForm({
      dataType: 'json',
      beforeSubmit: fileUploadBeforeSubmitCallback,
      success: fileUploadedCallback,
      error: fileUploadErrorCallback,
      uploadProgress: fileUploadProgressCallback
    });

    $selectFile.append($fileSelectTitle);
    $selectFile.append($uploadForm);

    var $label = $('').addClass('cabinet'),
      $fileInput = $('<input type="file" class="file" name="file"/>'),
      $pathInput = $('<input type="hidden" name="path" value="' + root + '"/>'),
      $submitInput = $('<button type="submit">').addClass('submitBtn').addClass('hide').text('Upload File');

    // bind click event of file name div to input file selector
    $fileSelectTitle.click(function() {
      $fileInput.click();
    });

    $uploadForm.append($label).append($pathInput).append($submitInput);
    $label.append($fileInput);
    $fileInput.attr("id", this.htmlObject + "_file");
    $fileInput.change(function() {
      if ($fileInput.val() !== "") {
        $selectFile.addClass('zeroHeight');
        $label.addClass('hide');
        $fileSelectTitle.addClass('hide');
        $fileObj.removeClass('hide');
        $submitInput.removeClass('hide');
        $cancelButton.removeClass('hide');

        var a = $fileInput.val();
        if (a.slice(3, 11) == "fakepath") a = a.slice(12, a.length);
        $fileObj.html(a);
      }
    });

    var $cancelButton = $('<button>').text('Cancel');
    $cancelButton.click(function() {
      $selectFile.removeClass('zeroHeight');
      $label.removeClass('hide');
      $fileSelectTitle.removeClass('hide');
      $fileObj.addClass('hide');
      $fileObj.html('');
      $submitInput.addClass('hide');
      $fileInput.val('');
      $cancelButton.addClass('hide');
    });

    $cancelButtonDiv.append($cancelButton);
    $cancelButton.addClass('hide');
  }
});<commit_msg>[CLEANUP] - Fixing visual bug in upload button<commit_after>'use strict';

var FileUploaderComponent = BaseComponent.extend({
  update: function() {
    var myself = this,
      $ph = $("#" + this.htmlObject),
      root = this.rootFolder.charAt(this.rootFolder.length - 1) === "/" ? this.rootFolder.substring(0, this.rootFolder.length - 1) : this.rootFolder;

    var $fileSelect = $('

').addClass('fileSelect');
    $ph.empty();
    $ph.addClass('fileUploaderComponent');
    $ph.append($fileSelect);

    var $selectFile = $('

').addClass('uploadButton');
    var $fileObj = $('

').addClass('hide').addClass('fileTextRow');
    var $cancelButtonDiv = $('

').addClass('cancelButton');

    /*
     * initialize upload dialog container
     */
    var $uploadDialogContainer = $('

');
    var $uploadDialogHeader = $('

').addClass('popupHeader');
    var $uploadDialogBody = $('

').addClass('popupBody');
    var $uploadDialogFooter = $('

').addClass('popupFooter');

    // dialog header
    var $uploadPopupCloseButton = $('<button class="popupButton">×</button>');
    $uploadDialogHeader.append($uploadPopupCloseButton);
    var $uploadDialogHeaderTitle = $('
Uploading File
');
    $uploadDialogHeader.append($uploadDialogHeaderTitle);
    $uploadDialogContainer.append($uploadDialogHeader);

    // upload progress bar
    var $uploadProgressBarContainer = $('

').addClass('uploadBar');
    var $uploadProgressBar = $('

');

    $uploadProgressBarContainer.append($uploadProgressBar);
    $uploadDialogBody.append($uploadProgressBarContainer);

    // upload alert section
    var $uploadAlerts = $('

');
    $uploadDialogBody.append($uploadAlerts);

    $uploadDialogContainer.append($uploadDialogBody);

    var $uploadDialogDismissButton = $('<button type="button" class="popupButton" disabled>OK</button>');

    $uploadDialogFooter.append($uploadDialogDismissButton);

    $uploadDialogContainer.append($uploadDialogFooter);

    var $popupContentContainer = $('

');
    $popupContentContainer.append($uploadDialogContainer);
    $ph.append($popupContentContainer);

    // END: initialize upload dialog container

    var popup = new PopupComponent();
    popup.htmlObject = "uploaderPopupContainer";
    popup.draggable = false;
    popup.closeOnClickOutside = true;
    popup.popupClass = "uploadPopup";
    popup.popupOverlayClass = "fileUploaderComponentOverlay";
    popup.update();
    popup.ph.find(".close").remove();

    $uploadPopupCloseButton.click(function(event) {
      popup.hide();
    });
    $uploadDialogDismissButton.click(function(event) {
      popup.hide();
    });

    $fileSelect.append($selectFile)
      .append($fileObj).append($cancelButtonDiv);

    var $fileSelectTitle = $('

').text('Select File').addClass('select');
    var $uploadForm = $('<form action="' + Endpoints.getStore() + '" method="post" enctype="multipart/form-data"></form>');
    var resetUploadForm = function() {
      $cancelButton.click();
    };

    var fileUploadedCallback = function(response) {
      if (response.result) {

        fileUploadProgressCallback(null, 0, 0, 100);

        // update alert
        $uploadAlerts.attr('class', 'alert success').text('File successfully uploaded');

        // activate upload dialog OK button
        $uploadDialogDismissButton.enable();

        //reset upload progress bar
        //fileUploadProgressCallback(null, 0, 0, 0);

        fireUploaded();
        resetUploadForm();
      } else {
        fileUploadErrorCallback()
      }
    };

    var fileUploadErrorCallback = function() {
      // update alert
      $uploadAlerts.attr('class', 'alert error').text('Error uploading file');
      // activate upload dialog OK button
      $uploadDialogDismissButton.enable();
    };

    var fileUploadProgressCallback = function(event, position, total, percent) {
      $uploadProgressBar.width(percent +"%");
    };

    var fileUploadBeforeSubmitCallback = function(arr, form, options) {
      // reset upload bar
      fileUploadProgressCallback(null, 0, 0, 0);

      // reset alerts
      $uploadAlerts.attr('class', 'hide').text('');

      // show upload dialog box
      popup.popup($ph);
      popup.ph.attr("style", "top: 15px; left:10px; right:10px; position:fixed;");
    };

    var fireUploaded = function(filename) {
      Dashboards.fireChange('uploadedFileParam', '');
    };

    // configure file upload form
    $uploadForm.ajaxForm({
      dataType: 'json',
      beforeSubmit: fileUploadBeforeSubmitCallback,
      success: fileUploadedCallback,
      error: fileUploadErrorCallback,
      uploadProgress: fileUploadProgressCallback
    });

    $selectFile.append($fileSelectTitle);
    $selectFile.append($uploadForm);

    var $label = $('').addClass('cabinet'),
      $fileInput = $('<input type="file" class="file" name="file"/>'),
      $pathInput = $('<input type="hidden" name="path" value="' + root + '"/>'),
      $submitInput = $('<button type="submit" class="submitBtn hide">Upload File</button>');

    // bind click event of file name div to input file selector
    $fileSelectTitle.click(function() {
      $fileInput.click();
    });

    $uploadForm.append($label).append($pathInput).append($submitInput);
    $label.append($fileInput);
    $fileInput.attr("id", this.htmlObject + "_file");
    $fileInput.change(function() {
      if ($fileInput.val() !== "") {
        $selectFile.addClass('zeroHeight');
        $label.addClass('hide');
        $fileSelectTitle.addClass('hide');
        $fileObj.removeClass('hide');
        $submitInput.removeClass('hide');
        $cancelButton.removeClass('hide');

        var a = $fileInput.val();
        if (a.slice(3, 11) == "fakepath") a = a.slice(12, a.length);
        $fileObj.html(a);
      }
    });

    var $cancelButton = $('<button>').text('Cancel');
    $cancelButton.click(function() {
      $selectFile.removeClass('zeroHeight');
      $label.removeClass('hide');
      $fileSelectTitle.removeClass('hide');
      $fileObj.addClass('hide');
      $fileObj.html('');
      $submitInput.addClass('hide');
      $fileInput.val('');
      $cancelButton.addClass('hide');
    });

    $cancelButtonDiv.append($cancelButton);
    $cancelButton.addClass('hide');
  }
});<|endoftext|>


<commit_before>'use strict'

const _ = require('lodash')

class PullRequests {
  constructor(prs, owner, repo) {
    this.prs = prs
    this.owner = owner
    this.repo = repo

    _.bindAll(this, ['belongsToOwner', 'matchesRepo'])
  }

  isIgnorable(pr) {
    const ignoreWords = ['wip', 'dont merge', 'dontmerge', 'donotmerge']
    const regex = new RegExp(`(${ignoreWords.join('|')})`, 'i')
    return !!pr.title.match(regex)
  }

  belongsToOwner(pr) {
    if (this.owner.value) {
      const result = pr.html_url.match('^https://github.com/([^/]+)/')[1] === this.owner.value
      return (this.owner.inclusion ? result : !result)
    } else {
      return true
    }
  }

  matchesRepo(pr) {
    if (this.repo.value.length > 0) {
      const result = _.some(this.repo.value, (repo) => {
        return pr.html_url.match('^https://github.com/([^/]+/[^/]+)/')[1] === repo
      })
      return (this.repo.inclusion ? result : !result)
    } else {
      return true
    }
  }

  formatPullRequest(pr, index) {
    return `${index+1}. \`${pr.title}\` ${pr.html_url} by ${pr.user.login}`
  }

  convertToSlackMessages() {
    return _(this.prs).flatMap((prs) => prs.data.items)
      .reject(this.isIgnorable)
      .filter(this.belongsToOwner)
      .filter(this.matchesRepo)
      .map(this.formatPullRequest)
      .value()
  }
}

module.exports = PullRequests
<commit_msg>Improve pattern matching of title for ignorable PRs<commit_after>'use strict'

const _ = require('lodash')

class PullRequests {
  constructor(prs, owner, repo) {
    this.prs = prs
    this.owner = owner
    this.repo = repo

    _.bindAll(this, ['belongsToOwner', 'matchesRepo'])
  }

  isIgnorable(pr) {
    const ignoreWords = ['wip', 'dontmerge', 'donotmerge']
    const regex = new RegExp(`(${ignoreWords.join('|')})`, 'i')
    const sanitizedTitle = pr.title.replace(/'|\s+/g, '')
    return !!sanitizedTitle.match(regex)
  }

  belongsToOwner(pr) {
    if (this.owner.value) {
      const result = pr.html_url.match('^https://github.com/([^/]+)/')[1] === this.owner.value
      return (this.owner.inclusion ? result : !result)
    } else {
      return true
    }
  }

  matchesRepo(pr) {
    if (this.repo.value.length > 0) {
      const result = _.some(this.repo.value, (repo) => {
        return pr.html_url.match('^https://github.com/([^/]+/[^/]+)/')[1] === repo
      })
      return (this.repo.inclusion ? result : !result)
    } else {
      return true
    }
  }

  formatPullRequest(pr, index) {
    return `${index+1}. \`${pr.title}\` ${pr.html_url} by ${pr.user.login}`
  }

  convertToSlackMessages() {
    return _(this.prs).flatMap((prs) => prs.data.items)
      .reject(this.isIgnorable)
      .filter(this.belongsToOwner)
      .filter(this.matchesRepo)
      .map(this.formatPullRequest)
      .value()
  }
}

module.exports = PullRequests
<|endoftext|>


<commit_before>/**
 * Component definition for the frost-table component
 */
import Ember from 'ember'
const {A, isNone} = Ember
import computed, {readOnly} from 'ember-computed-decorators'
import {Component} from 'ember-frost-core'
import {ColumnPropType, ItemPropType, ItemsPropType} from 'ember-frost-table/typedefs'
import {PropTypes} from 'ember-prop-types'

import layout from '../templates/components/frost-table'
import {select} from '../utils/selection'

export default Component.extend({
  // == Dependencies ==========================================================

  // == Keyword Properties ====================================================

  classNameBindings: ['_isShiftDown:shift-down'],
  layout,
  tagName: 'table',

  // == PropTypes =============================================================

  propTypes: {
    // options
    columns: PropTypes.arrayOf(ColumnPropType).isRequired,
    itemKey: PropTypes.string,
    items: ItemsPropType.isRequired,
    selectedItems: PropTypes.arrayOf(ItemPropType),

    // callbacks
    onCallback: PropTypes.func,
    onSelectionChange: PropTypes.func,

    // state
    _isShiftDown: PropTypes.bool,
    _itemComparator: PropTypes.func,

    _rangeState: PropTypes.shape({
      anchor: PropTypes.oneOfType([
        PropTypes.EmberObject,
        PropTypes.object
      ]),
      endpoint: PropTypes.oneOfType([
        PropTypes.EmberObject,
        PropTypes.object
      ])
    })
  },

  getDefaultProps () {
    return {
      // options
      columns: [],
      items: [],
      onCallback () {},

      // state
      _rangeState: {
        anchor: null,
        endpoint: null
      }
    }
  },

  // == Computed Properties ===================================================

  @readOnly
  @computed('columns')
  /**
   * Pre-computed indices
   * @param {Column[]} columns - the column data we want to present
   * @returns {Column[]} indexed - columns with an 'index' property added
   */
  indexedColumns (columns) {
    return columns.map((column, index) => Object.assign({index}, column))
  },

  @readOnly
  @computed()
  _isSelectable () {
    return !isNone(this.get('onSelectionChange'))
  },

  // == Functions =============================================================

  setShift (event) {
    if (!this.isDestroyed) {
      this.set('_isShiftDown', event.shiftKey)
    }
  },

  setCellWidths (position) {
    const cellRowSelector = this.$('.has-categories').length === 1 ? '.frost-table-header-columns' : ''
    const curBodyColumn = this.$(`.frost-table-row .frost-table-body-cell:nth-child(${position})`)
    const curHeaderCell = this.$(`.frost-table-header ${cellRowSelector} .frost-table-cell:nth-child(${position})`)

    const bodyCellWidth = curBodyColumn.outerWidth(true)
    const headerCellWidth = curHeaderCell.outerWidth(true)

    const width = bodyCellWidth > headerCellWidth ? bodyCellWidth : headerCellWidth

    curHeaderCell.css('width', `${width}px`)
    curBodyColumn.css('width', `${width}px`)

    return width
  },

  accountForSelectionColumn (num) {
    if (this.get('isSelectable')) {
      return num + 1
    }
    return num
  },

  // == DOM Events ============================================================

  // == Lifecycle Hooks =======================================================

  didRender () {
    const selectable = this.get('_isSelectable')
    let totalWidth = 0
    if (selectable) {
      totalWidth += this.setCellWidths(1)
    }
    this.columns.forEach((column, index) => {
      const position = index + this.accountForSelectionColumn(1)
      totalWidth += this.setCellWidths(position)
    })
    this.$('.frost-table-header').css('width', `${totalWidth}px`)
    this.$('.frost-table-body').css('width', `${totalWidth}px`)
  },

  // == Actions ===============================================================

  actions: {
    /**
     * Wrap our arguments in a single object, so that cell renderers can trigger arbitrary events.
     * Your handler is then responsible for doing stuff based on actions like 'click' or 'input'.
     *
     * @param {Number} row - data rows are zero based, and header has row -1
     * @param {Number} col - column index
     * @param {String} action - this comes after row/col as Ember lets us include those in action closures easily.
     * @param {Object[]} args - any additional data
     */
    handleCallback (row, col, action, args) {
      const handler = this.get('onCallback')
      if (handler) {
        handler({action, row, col, args})
      }
    },

    _select ({isRangeSelect, isSpecificSelect, item}) {
      const items = this.get('items')
      const itemKey = this.get('itemKey')
      const clonedSelectedItems = A(this.get('selectedItems').slice())
      const _rangeState = this.get('_rangeState')

      select(isRangeSelect, isSpecificSelect, item, itemKey, items, clonedSelectedItems, _rangeState)

      this.onSelectionChange(clonedSelectedItems)
    }
  }
})
<commit_msg>Fix column alignment issue<commit_after>/**
 * Component definition for the frost-table component
 */
import Ember from 'ember'
const {A, isNone} = Ember
import computed, {readOnly} from 'ember-computed-decorators'
import {Component} from 'ember-frost-core'
import {ColumnPropType, ItemPropType, ItemsPropType} from 'ember-frost-table/typedefs'
import {PropTypes} from 'ember-prop-types'

import layout from '../templates/components/frost-table'
import {select} from '../utils/selection'

export default Component.extend({
  // == Dependencies ==========================================================

  // == Keyword Properties ====================================================

  classNameBindings: ['_isShiftDown:shift-down'],
  layout,
  tagName: 'table',

  // == PropTypes =============================================================

  propTypes: {
    // options
    columns: PropTypes.arrayOf(ColumnPropType).isRequired,
    itemKey: PropTypes.string,
    items: ItemsPropType.isRequired,
    selectedItems: PropTypes.arrayOf(ItemPropType),

    // callbacks
    onCallback: PropTypes.func,
    onSelectionChange: PropTypes.func,

    // state
    _isShiftDown: PropTypes.bool,
    _itemComparator: PropTypes.func,

    _rangeState: PropTypes.shape({
      anchor: PropTypes.oneOfType([
        PropTypes.EmberObject,
        PropTypes.object
      ]),
      endpoint: PropTypes.oneOfType([
        PropTypes.EmberObject,
        PropTypes.object
      ])
    })
  },

  getDefaultProps () {
    return {
      // options
      columns: [],
      items: [],
      onCallback () {},

      // state
      _rangeState: {
        anchor: null,
        endpoint: null
      }
    }
  },

  // == Computed Properties ===================================================

  @readOnly
  @computed('columns')
  /**
   * Pre-computed indices
   * @param {Column[]} columns - the column data we want to present
   * @returns {Column[]} indexed - columns with an 'index' property added
   */
  indexedColumns (columns) {
    return columns.map((column, index) => Object.assign({index}, column))
  },

  @readOnly
  @computed()
  _isSelectable () {
    return !isNone(this.get('onSelectionChange'))
  },

  // == Functions =============================================================

  setShift (event) {
    if (!this.isDestroyed) {
      this.set('_isShiftDown', event.shiftKey)
    }
  },

  setCellWidths (position) {
    const cellRowSelector = this.$('.has-categories').length === 1 ? '.frost-table-header-columns' : ''
    const curBodyColumn = this.$(`.frost-table-row .frost-table-body-cell:nth-child(${position})`)
    const curHeaderCell = this.$(`.frost-table-header ${cellRowSelector} .frost-table-cell:nth-child(${position})`)

    const bodyCellWidth = curBodyColumn.outerWidth(true)
    const headerCellWidth = curHeaderCell.outerWidth(true)

    const width = bodyCellWidth > headerCellWidth ? bodyCellWidth : headerCellWidth

    curHeaderCell.css('width', `${width}px`)
    curBodyColumn.css('width', `${width}px`)

    return width
  },

  accountForSelectionColumn (num) {
    if (this.get('_isSelectable')) {
      return num + 1
    }
    return num
  },

  // == DOM Events ============================================================

  // == Lifecycle Hooks =======================================================

  didRender () {
    const selectable = this.get('_isSelectable')
    let totalWidth = 0
    if (selectable) {
      totalWidth += this.setCellWidths(1)
    }
    this.columns.forEach((column, index) => {
      const position = index + this.accountForSelectionColumn(1)
      totalWidth += this.setCellWidths(position)
    })
    this.$('.frost-table-header').css('width', `${totalWidth}px`)
    this.$('.frost-table-body').css('width', `${totalWidth}px`)
  },

  // == Actions ===============================================================

  actions: {
    /**
     * Wrap our arguments in a single object, so that cell renderers can trigger arbitrary events.
     * Your handler is then responsible for doing stuff based on actions like 'click' or 'input'.
     *
     * @param {Number} row - data rows are zero based, and header has row -1
     * @param {Number} col - column index
     * @param {String} action - this comes after row/col as Ember lets us include those in action closures easily.
     * @param {Object[]} args - any additional data
     */
    handleCallback (row, col, action, args) {
      const handler = this.get('onCallback')
      if (handler) {
        handler({action, row, col, args})
      }
    },

    _select ({isRangeSelect, isSpecificSelect, item}) {
      const items = this.get('items')
      const itemKey = this.get('itemKey')
      const clonedSelectedItems = A(this.get('selectedItems').slice())
      const _rangeState = this.get('_rangeState')

      select(isRangeSelect, isSpecificSelect, item, itemKey, items, clonedSelectedItems, _rangeState)

      this.onSelectionChange(clonedSelectedItems)
    }
  }
})
<|endoftext|>


<commit_before>import Ember from "ember";
import SelectMenuLabel from "../views/select-menu-label";
import nearestChild from "ember-popup-menu/computed/nearest-child";
import w from "ember-popup-menu/computed/w";
import { getLayout } from "dom-ruler";

var get = Ember.get;
var next = Ember.run.next;
var cancel = Ember.run.cancel;
var later = Ember.run.later;
var set = Ember.set;

var guidFor = Ember.guidFor;

var filterBy = Ember.computed.filterBy;
var reads = Ember.computed.reads;
var alias = Ember.computed.alias;

var RSVP = Ember.RSVP;
var $ = Ember.$;

// Key code mappings
var ESC              = 27,
    UP               = 38,
    DOWN             = 40,
    BACKSPACE        = 8,
    TAB              = 9,
    ENTER            = 13,
    SHIFT            = 16,
    CTRL             = 17,
    ALT              = 18,
    CAPS_LOCK        = 20,
    PAGE_DOWN        = 33,
    PAGE_UP          = 34,
    END              = 35,
    INSERT           = 36,
    DELETE           = 46,
    LEFT_WINDOW_KEY  = 91,
    RIGHT_WINDOW_KEY = 92,
    SELECT_KEY       = 93,
    NUM_LOCK         = 144,
    SCROLL_LOCK      = 145;

var SelectMenu = Ember.Component.extend({

  init: function () {
    if (get(this, 'elementId') !== guidFor(this)) {
      set(this, 'inheritedId', get(this, 'elementId'));
      set(this, 'elementId', guidFor(this));
    }

    this._super();
  },

  createChildView: function (childView, options) {
    if (SelectMenuLabel.detect(childView)) {
      options.elementId = get(this, 'inheritedId');
    }
    return this._super(childView, options);
  },

  classNames: ['select-menu'],

  disabled: false,

  options: function () {
    return [];
  }.property(),

  activeOptions: filterBy('options', 'disabled', false),

  activeDescendants: filterBy('options', 'selected'),
  activeDescendant: reads('activeDescendants.firstObject'),

  searchString: null,
  isActive: alias('popup.isActive'),

  prompt: null,

  label: nearestChild('select-menu-label'),
  list: nearestChild('select-menu-list'),
  popup: nearestChild('popup-menu'),

  addTarget: function () {
    next(this, function () {
      get(this, 'popup').addTarget(get(this, 'label'), {
        on: "click hold"
      });
    });
  }.on('didInsertElement'),

  /**
    The item of the content that is currently selected.

    If the {{select-menu}} has a prompt, then the value
    will by default be null. Otherwise, the value will
    be the first item in the content.

    @property value
    @type Object
    @default null
   */
  value: function (key, value) {
    if (value && value.then) {
      var menu = this;
      RSVP.Promise.cast(value).then(function (unwrappedValue) {
        if (menu.isDestroyed) { return; }
        set(menu, 'value', unwrappedValue);
      });
    } else if (value == null) {
      next(this, function () {
        if (this.isDestroyed || get(this, 'prompt')) { return; }
        var firstOption = get(this, 'options.firstObject.value');
        if (firstOption) {
          set(this, 'value', firstOption);
        }
      });
    }

    return value;
  }.property(),

  _shouldShowPrompt: function () {
    var hasPrompt = !!get(this, 'prompt');
    if (!hasPrompt && get(this, 'value') == null) {
      this.notifyPropertyChange('value');
    }
  }.observes('options.[]', 'prompt').on('init'),

  /**
    Interpret keyboard events
   */
  keyDown: function (evt) {
    var code = (evt.keyCode ? evt.keyCode : evt.which);
    var search = get(this, 'searchString');
    var label = get(this, 'label');
    var popup = get(this, 'popup');
    var isActive = get(this, 'isActive');

    // If the meta key was held, don't do anything.
    if (evt.metaKey) {
      if (String.fromCharCode(code).toLowerCase() === 'a') {
        code = SELECT_KEY;
      } else {
        return;
      }
    }

    // Ignore all events if the select is disabled
    if (get(this, 'disabled')) { return; }

    switch (code) {
    case UP:
      if (isActive) {
        this.selectPrevious();
      }
      popup.activate(label);

      break;
    case DOWN:
      if (isActive) {
        this.selectNext();
      }
      popup.activate(label);

      break;
    case ESC:
      popup.deactivate();

      break;

    // Allow tabs to pass through
    case TAB:
    case ENTER:
      popup.deactivate();
      return;

    // A whitelist of characters to let the browser handle
    case SHIFT:
    case CTRL:
    case ALT:
    case CAPS_LOCK:
    case PAGE_DOWN:
    case PAGE_UP:
    case END:
    case INSERT:
    case DELETE:
    case LEFT_WINDOW_KEY:
    case RIGHT_WINDOW_KEY:
    case NUM_LOCK:
    case SCROLL_LOCK:
      return;

    case SELECT_KEY:
      break;

    case BACKSPACE:
      if (search) {
        set(this, 'searchString', search.slice(0, -1));
      }

      break;
    default:
      var chr = String.fromCharCode(code);

      // Append
      if (search) {
        set(this, 'searchString', search + chr);
      } else {
        if (chr === ' ') {
          if (isActive) {
            popup.deactivate();
          } else {
            popup.activate(label);
          }
        } else {
          popup.activate(label);
          set(this, 'searchString', chr);
        }
      }
    }

    evt.preventDefault();
  },

  /**
    Selects the next item in the option list.
   */
  selectNext: function () {
    var options = get(this, 'activeOptions');
    var activeDescendant = get(this, 'activeDescendant');
    var index;

    if (options) {
      if (activeDescendant) {
        index = options.indexOf(activeDescendant);
      } else {
        index = -1;
      }

      var option = options.objectAt(Math.min(index + 1, get(options, 'length') - 1));
      if (option !== activeDescendant) {
        set(this, 'activeDescendant', option);
        set(this, 'value', get(option, 'value'));
      }
    }
  },

  /**
    Selects the previous item in the option list.
   */
  selectPrevious: function () {
    var options = get(this, 'activeOptions');
    var activeDescendant = get(this, 'activeDescendant');
    var index;

    if (options) {
      if (activeDescendant) {
        index = options.indexOf(activeDescendant);
      } else {
        index = get(options, 'length');
      }

      var option = options.objectAt(Math.max(index - 1, 0));
      if (option !== activeDescendant) {
        set(this, 'activeDescendant', option);
        set(this, 'value', get(option, 'value'));
      }
    }
  },

  /**
    Search by value of the object
   */
  "search-by": alias('searchBy'),
  searchBy: w("label"),

  /**
    Locally iterate through options and find the
    best match. After 750 milliseconds of inactivity,
    the search is reset, allowing users to search again.
   */
  searchStringDidChange: function () {
    if (this.__timer) {
      cancel(this.__timer);
    }

    var options = get(this, 'activeOptions');
    var search = get(this, 'searchString');
    var searchBy = get(this, 'searchBy');

    if (options && search && searchBy) {
      var length = get(options, 'length'),
          match = null,
          start,
          matchIndex;

      search = search.toUpperCase();

      // Continue searching from the index of
      // the last search match
      if (this.__matchIndex) {
        start = Math.min(this.__matchIndex, length - 1);
      } else {
        start = 0;
      }

      var hasMatch = function (option) {
        for (var i = 0; i < searchBy.length; i++) {
          if (String(get(option, searchBy[i]) || '').toUpperCase().indexOf(search) === 0) {
            return true;
          }
        }
        return false;
      };

      // Search from the current value
      // for the next match
      for (var i = start; i < length; i++) {
        var option = options.objectAt(i);
        match = hasMatch(option);

        // Break on the first match,
        // if a user would like to match
        // a more specific entry, they should
        // continue typing
        if (match) {
          match = option;
          matchIndex = i;
          break;
        }
      }

      // Stash the last matched search item
      // so we can continue searching from that
      // index on consective searches
      if (match != null) {
        set(this, 'activeDescendant', match);
        set(this, 'value', get(match, 'value'));
        this.__matchIndex = matchIndex;
      }
    }

    if (search) {
      this.__timer = later(this, this.resetSearch, 750);
    }
  }.observes('searchString'),

  /**
    Reset the `searchString`.
   */
  resetSearch: function () {
    this.__timer = null;
    this.__matchIndex = null;
    set(this, 'searchString', null);
  },

  scrollActiveDescendantIntoView: function () {
    var activeDescendant = get(this, 'activeDescendant');

    if (activeDescendant && get(this, 'isActive') && get(this, 'list')) {
      var list = get(this, 'list');
      var listBox = getLayout(get(this, 'list.element'));
      var height = getLayout(get(this, 'popup.element')).padding.height;
      var option = get(activeDescendant, 'element');
      var scrollTop = list.$().scrollTop();
      var scrollBottom = scrollTop + listBox.padding.height;

      var optionTop = scrollTop + $(option).position().top;
      var optionBottom = optionTop + getLayout(option).margins.height;

      if (optionTop < scrollTop) {
        list.$().scrollTop(optionTop - listBox.padding.top);
      } else if (optionBottom > scrollBottom) {
        list.$().scrollTop(optionBottom - height + listBox.padding.bottom);
      }
    }
  }.observes('activeDescendant').on('init')

});

export default SelectMenu;
<commit_msg>prevent aggressive setting of the value<commit_after>import Ember from "ember";
import SelectMenuLabel from "../views/select-menu-label";
import nearestChild from "ember-popup-menu/computed/nearest-child";
import w from "ember-popup-menu/computed/w";
import { getLayout } from "dom-ruler";

var get = Ember.get;
var next = Ember.run.next;
var cancel = Ember.run.cancel;
var later = Ember.run.later;
var set = Ember.set;

var guidFor = Ember.guidFor;

var filterBy = Ember.computed.filterBy;
var reads = Ember.computed.reads;
var alias = Ember.computed.alias;

var RSVP = Ember.RSVP;
var $ = Ember.$;

// Key code mappings
var ESC              = 27,
    UP               = 38,
    DOWN             = 40,
    BACKSPACE        = 8,
    TAB              = 9,
    ENTER            = 13,
    SHIFT            = 16,
    CTRL             = 17,
    ALT              = 18,
    CAPS_LOCK        = 20,
    PAGE_DOWN        = 33,
    PAGE_UP          = 34,
    END              = 35,
    INSERT           = 36,
    DELETE           = 46,
    LEFT_WINDOW_KEY  = 91,
    RIGHT_WINDOW_KEY = 92,
    SELECT_KEY       = 93,
    NUM_LOCK         = 144,
    SCROLL_LOCK      = 145;

var SelectMenu = Ember.Component.extend({

  init: function () {
    if (get(this, 'elementId') !== guidFor(this)) {
      set(this, 'inheritedId', get(this, 'elementId'));
      set(this, 'elementId', guidFor(this));
    }

    this._super();
  },

  createChildView: function (childView, options) {
    if (SelectMenuLabel.detect(childView)) {
      options.elementId = get(this, 'inheritedId');
    }
    return this._super(childView, options);
  },

  classNames: ['select-menu'],

  disabled: false,

  options: function () {
    return [];
  }.property(),

  activeOptions: filterBy('options', 'disabled', false),

  activeDescendants: filterBy('options', 'selected'),
  activeDescendant: reads('activeDescendants.firstObject'),

  searchString: null,
  isActive: alias('popup.isActive'),

  prompt: null,

  label: nearestChild('select-menu-label'),
  list: nearestChild('select-menu-list'),
  popup: nearestChild('popup-menu'),

  addTarget: function () {
    next(this, function () {
      get(this, 'popup').addTarget(get(this, 'label'), {
        on: "click hold"
      });
    });
  }.on('didInsertElement'),

  /**
    The item of the content that is currently selected.

    If the {{select-menu}} has a prompt, then the value
    will by default be null. Otherwise, the value will
    be the first item in the content.

    @property value
    @type Object
    @default null
   */
  value: function (key, value) {
    cancel(this._value$timer);

    if (value && value.then) {
      var menu = this;
      RSVP.Promise.cast(value).then(function (unwrappedValue) {
        if (menu.isDestroyed) { return; }
        set(menu, 'value', unwrappedValue);
      });
    } else if (value == null) {
      this._value$timer = next(this, function () {
        if (this.isDestroyed || get(this, 'prompt')) { return; }
        var firstOption = get(this, 'options.firstObject.value');
        if (firstOption && this._value$value == null) {
          set(this, 'value', firstOption);
        }
      });
    }

    this._value$value = value;
    return value;
  }.property(),

  _shouldShowPrompt: function () {
    var hasPrompt = !!get(this, 'prompt');
    if (!hasPrompt && get(this, 'value') == null) {
      this.notifyPropertyChange('value');
    }
  }.observes('options.[]', 'prompt').on('init'),

  /**
    Interpret keyboard events
   */
  keyDown: function (evt) {
    var code = (evt.keyCode ? evt.keyCode : evt.which);
    var search = get(this, 'searchString');
    var label = get(this, 'label');
    var popup = get(this, 'popup');
    var isActive = get(this, 'isActive');

    // If the meta key was held, don't do anything.
    if (evt.metaKey) {
      if (String.fromCharCode(code).toLowerCase() === 'a') {
        code = SELECT_KEY;
      } else {
        return;
      }
    }

    // Ignore all events if the select is disabled
    if (get(this, 'disabled')) { return; }

    switch (code) {
    case UP:
      if (isActive) {
        this.selectPrevious();
      }
      popup.activate(label);

      break;
    case DOWN:
      if (isActive) {
        this.selectNext();
      }
      popup.activate(label);

      break;
    case ESC:
      popup.deactivate();

      break;

    // Allow tabs to pass through
    case TAB:
    case ENTER:
      popup.deactivate();
      return;

    // A whitelist of characters to let the browser handle
    case SHIFT:
    case CTRL:
    case ALT:
    case CAPS_LOCK:
    case PAGE_DOWN:
    case PAGE_UP:
    case END:
    case INSERT:
    case DELETE:
    case LEFT_WINDOW_KEY:
    case RIGHT_WINDOW_KEY:
    case NUM_LOCK:
    case SCROLL_LOCK:
      return;

    case SELECT_KEY:
      break;

    case BACKSPACE:
      if (search) {
        set(this, 'searchString', search.slice(0, -1));
      }

      break;
    default:
      var chr = String.fromCharCode(code);

      // Append
      if (search) {
        set(this, 'searchString', search + chr);
      } else {
        if (chr === ' ') {
          if (isActive) {
            popup.deactivate();
          } else {
            popup.activate(label);
          }
        } else {
          popup.activate(label);
          set(this, 'searchString', chr);
        }
      }
    }

    evt.preventDefault();
  },

  /**
    Selects the next item in the option list.
   */
  selectNext: function () {
    var options = get(this, 'activeOptions');
    var activeDescendant = get(this, 'activeDescendant');
    var index;

    if (options) {
      if (activeDescendant) {
        index = options.indexOf(activeDescendant);
      } else {
        index = -1;
      }

      var option = options.objectAt(Math.min(index + 1, get(options, 'length') - 1));
      if (option !== activeDescendant) {
        set(this, 'activeDescendant', option);
        set(this, 'value', get(option, 'value'));
      }
    }
  },

  /**
    Selects the previous item in the option list.
   */
  selectPrevious: function () {
    var options = get(this, 'activeOptions');
    var activeDescendant = get(this, 'activeDescendant');
    var index;

    if (options) {
      if (activeDescendant) {
        index = options.indexOf(activeDescendant);
      } else {
        index = get(options, 'length');
      }

      var option = options.objectAt(Math.max(index - 1, 0));
      if (option !== activeDescendant) {
        set(this, 'activeDescendant', option);
        set(this, 'value', get(option, 'value'));
      }
    }
  },

  /**
    Search by value of the object
   */
  "search-by": alias('searchBy'),
  searchBy: w("label"),

  /**
    Locally iterate through options and find the
    best match. After 750 milliseconds of inactivity,
    the search is reset, allowing users to search again.
   */
  searchStringDidChange: function () {
    if (this.__timer) {
      cancel(this.__timer);
    }

    var options = get(this, 'activeOptions');
    var search = get(this, 'searchString');
    var searchBy = get(this, 'searchBy');

    if (options && search && searchBy) {
      var length = get(options, 'length'),
          match = null,
          start,
          matchIndex;

      search = search.toUpperCase();

      // Continue searching from the index of
      // the last search match
      if (this.__matchIndex) {
        start = Math.min(this.__matchIndex, length - 1);
      } else {
        start = 0;
      }

      var hasMatch = function (option) {
        for (var i = 0; i < searchBy.length; i++) {
          if (String(get(option, searchBy[i]) || '').toUpperCase().indexOf(search) === 0) {
            return true;
          }
        }
        return false;
      };

      // Search from the current value
      // for the next match
      for (var i = start; i < length; i++) {
        var option = options.objectAt(i);
        match = hasMatch(option);

        // Break on the first match,
        // if a user would like to match
        // a more specific entry, they should
        // continue typing
        if (match) {
          match = option;
          matchIndex = i;
          break;
        }
      }

      // Stash the last matched search item
      // so we can continue searching from that
      // index on consective searches
      if (match != null) {
        set(this, 'activeDescendant', match);
        set(this, 'value', get(match, 'value'));
        this.__matchIndex = matchIndex;
      }
    }

    if (search) {
      this.__timer = later(this, this.resetSearch, 750);
    }
  }.observes('searchString'),

  /**
    Reset the `searchString`.
   */
  resetSearch: function () {
    this.__timer = null;
    this.__matchIndex = null;
    set(this, 'searchString', null);
  },

  scrollActiveDescendantIntoView: function () {
    var activeDescendant = get(this, 'activeDescendant');

    if (activeDescendant && get(this, 'isActive') && get(this, 'list')) {
      var list = get(this, 'list');
      var listBox = getLayout(get(this, 'list.element'));
      var height = getLayout(get(this, 'popup.element')).padding.height;
      var option = get(activeDescendant, 'element');
      var scrollTop = list.$().scrollTop();
      var scrollBottom = scrollTop + listBox.padding.height;

      var optionTop = scrollTop + $(option).position().top;
      var optionBottom = optionTop + getLayout(option).margins.height;

      if (optionTop < scrollTop) {
        list.$().scrollTop(optionTop - listBox.padding.top);
      } else if (optionBottom > scrollBottom) {
        list.$().scrollTop(optionBottom - height + listBox.padding.bottom);
      }
    }
  }.observes('activeDescendant').on('init')

});

export default SelectMenu;
<|endoftext|>


<commit_before>/**
 * The PropTypes.shape validator
 */

import Ember from 'ember'
const {get, typeOf} = Ember

import logger from '../logger'

export default function (validators, ctx, name, value, def, logErrors, throwErrors) {
  const typeDefs = def.typeDefs
  let msg = `Expected property ${name} to match given shape`

  try {
    shape = JSON.stringify(value, null, ' ')
    msg = `${msg}, but instead got value ${shape}`
  } catch (e) {}

  if (typeOf(typeDefs) !== 'object') {
    logger.warn(ctx, 'PropTypes.shape() requires a plain object to be be passed in as an argument', throwErrors)
    return false
  }

  if (typeOf(value) !== 'object') {
    logger.warn(ctx, msg, throwErrors)
    return false
  }

  let valid = Object.keys(typeDefs).every((key) => {
    const typeDef = typeDefs[key]

    const objectValue = get(value, key)
    if (objectValue === undefined) {
      if (!typeDef.required) {
        return true
      } else {
        if (logErrors) {
          logger.warn(ctx, `Property ${name} is missing required property ${key}`, throwErrors)
        }
        return false
      }
    }

    return validators[typeDef.type](ctx, `${name}.${key}`, objectValue, typeDef, logErrors, throwErrors)
  })

  valid = valid && Object.keys(value).every((key) => {
    const keyIsKnown = (key in typeDefs)
    if (!keyIsKnown && logErrors) {
      logger.warn(ctx, `Property ${name} has an unknown key: ${key}`, throwErrors)
    }
    return keyIsKnown
  })

  if (!valid && logErrors) {
    logger.warn(ctx, msg, throwErrors)
  }

  return valid
}
<commit_msg>fix(bug): Added scope decl for shape<commit_after>/**
 * The PropTypes.shape validator
 */

import Ember from 'ember'
const {get, typeOf} = Ember

import logger from '../logger'

export default function (validators, ctx, name, value, def, logErrors, throwErrors) {
  const typeDefs = def.typeDefs
  let msg = `Expected property ${name} to match given shape`
  let shape
  try {
    shape = JSON.stringify(value, null, ' ')
    msg = `${msg}, but instead got value ${shape}`
  } catch (e) {}

  if (typeOf(typeDefs) !== 'object') {
    logger.warn(ctx, 'PropTypes.shape() requires a plain object to be be passed in as an argument', throwErrors)
    return false
  }

  if (typeOf(value) !== 'object') {
    logger.warn(ctx, msg, throwErrors)
    return false
  }

  let valid = Object.keys(typeDefs).every((key) => {
    const typeDef = typeDefs[key]

    const objectValue = get(value, key)
    if (objectValue === undefined) {
      if (!typeDef.required) {
        return true
      } else {
        if (logErrors) {
          logger.warn(ctx, `Property ${name} is missing required property ${key}`, throwErrors)
        }
        return false
      }
    }

    return validators[typeDef.type](ctx, `${name}.${key}`, objectValue, typeDef, logErrors, throwErrors)
  })

  valid = valid && Object.keys(value).every((key) => {
    const keyIsKnown = (key in typeDefs)
    if (!keyIsKnown && logErrors) {
      logger.warn(ctx, `Property ${name} has an unknown key: ${key}`, throwErrors)
    }
    return keyIsKnown
  })

  if (!valid && logErrors) {
    logger.warn(ctx, msg, throwErrors)
  }

  return valid
}
<|endoftext|>


<commit_before>(function($) {

    // Private var to assign IDs to history for accessibility purposes
    var historyCount = 0;

    // The close button template
    var $historyCloseButton = $('<button></button>')
                              .addClass('bc-history-button')
                              .find('abbr')
                                .addClass('only-icon')
                                .attr('title', gettext('Return to compatability table.'))
                                .end()
                              .find('span')
                                .append(gettext('Close'))
                              .end()
                              .find('i')
                                .addClass('icon-times')
                                .attr('aria-hidden', true)
                              .end();

    //  Usage: $('.compat-table').mozCompatTable();
    return jQuery.fn.mozCompatTable = function() {
        return $(this).each(function() {

            // Keep track of what may be open within this table
            var $openCell;

            // If nothing provided, bail
            var $table = $(this);
            if(!$table.length) return;

            // Activate all history cells for keyboard;  TODO:  add "aria-controls"
            $table.find('.bc-has-history').each(function() {
                var historyId = 'bc-history-' + (++historyCount);
                var $td = $(this);

                $td.attr({
                    tabIndex: 0,
                    'aria-expanded': false,
                    'aria-controls': historyId
                });
                $td.find('.bc-history').attr('id', historyId);
                $td.find('.bc-history-link').attr('tabIndex', -1);
            });

            // Listen for clicks on "history" cells
            $table.on('click', '.bc-has-history, .bc-history', function() {
                var $target = $(this);

                if($target.hasClass('.bc-history')) {
                    e.stopImmediatePropagation();
                }
                closeAndOpenHistory($target);
            });

            // Listen for clicks on "close" buttons
            $table.on('click', '.bc-history-button', function(ev) {
                ev.stopImmediatePropagation();
                hideHistory();
            });

            // Listen for keys to open/close history
            $table.on('keypress', '.bc-has-history', function(ev) {
                var $td = $(this);
                var key = ev.which;

                if(key === 13 || key === 32) {
                    closeAndOpenHistory($td);
                    ev.preventDefault();
                } else if(key === 27) {
                    hideHistory();
                    ev.preventDefault();
                }
            });

            // Function which closes any open history, opens the target history
            function closeAndOpenHistory($td) {
                var previousOpenCell = $openCell && $openCell.get(0);

                // Close what's open, if anything
                hideHistory();

                // If they clicks the same cell (are closing), we can leave now
                if($td.get(0) == previousOpenCell) {
                    $openCell = false;
                    return;
                }

                // Set this cell as open
                $openCell = $td;

                // Show!
                showHistory();
            }

            // Opens the history for a given item
            function showHistory() {

                var $row = $openCell.closest('tr');
                var $history = $openCell.find('.bc-history').outerWidth($table.width());

                // get cell coords
                var cellLeft = $openCell.offset().left;

                // get cell left border
                var cellLeftBorder = $openCell.css('border-left-width');

                // get table coords
                var tableLeft = $table.offset().left;

                // left coord of table minus left coord of cell
                var historyLeft = tableLeft - cellLeft - parseInt(cellLeftBorder, 10) - 1;

                // top
                // can't just to top:100% in CSS because IE messes it up.

                // get cell height
                var cellTop = $openCell.outerHeight();

                // get cell top border
                var celltopBorder = $openCell.css('border-top-width');

                // get cell bottom border
                var cellBottomBorder = $openCell.css('border-bottom-width');

                var historyTop = cellTop - parseInt(cellBottomBorder, 10) - parseInt(celltopBorder, 10);

                var historyHeight;
                var windowWidth;

                // Add a close button if one doesn't already exist
                if($history.find('.bc-history-button').length === 0) {
                    $historyCloseButton.clone().appendTo($history);
                }

                // move history where it will display
                $history.css({
                    left: historyLeft,
                    top: historyTop
                });

                // measure height
                $history.css('display', 'block');
                $history.attr('aria-hidden', false);

                historyHeight = $history.outerHeight();

                // set max-height to 0 and visibility to visible
                $openCell.addClass('active');
                $openCell.attr('aria-expanded', true);
                $history.css('height', historyHeight);

                // add measured height to history and to the cell/row it is being displayed beneath (CSS handles transition)
                windowWidth = window.innerWidth;
                if(windowWidth > 801) {
                    $row.find('th, td').css('border-bottom', historyHeight + 'px solid transparent');
                } if(windowWidth > 481) {
                    $row.find('td').css('border-bottom', historyHeight + 'px solid transparent');
                } else {
                    $openCell.css('border-bottom', historyHeight + 'px solid transparent');
                }
            }

            // Hides the history dropdown for a given cell
            function hideHistory(){
                if(!$openCell) return;

                var $history;

                $openCell.css('border-bottom', '').attr('aria-expanded', false);
                $openCell.closest('tr').find('th, td').css('border-bottom', '');

                $history = $openCell.find('.bc-history');
                $history.css('height', '').attr('aria-hidden', true);

                // if the focus is inside the .bc-history and we'd lose our keyboard place, move focus to parent
                if($.contains($openCell.get(0), document.activeElement)) {
                    $openCell.focus();
                }

                $openCell.removeClass('active');
                $history.css('display', 'none');
            }
        });
    };

})(jQuery);
<commit_msg>bug 1170235 - Add delay for table animation<commit_after>(function($) {

    // Private var to assign IDs to history for accessibility purposes
    var historyCount = 0;

    // The close button template
    var $historyCloseButton = $('<button></button>')
                              .addClass('bc-history-button')
                              .find('abbr')
                                .addClass('only-icon')
                                .attr('title', gettext('Return to compatability table.'))
                                .end()
                              .find('span')
                                .append(gettext('Close'))
                              .end()
                              .find('i')
                                .addClass('icon-times')
                                .attr('aria-hidden', true)
                              .end();

    //  Usage: $('.compat-table').mozCompatTable();
    return jQuery.fn.mozCompatTable = function() {
        return $(this).each(function() {

            // Keep track of what may be open within this table
            var $openCell;

            // If nothing provided, bail
            var $table = $(this);
            if(!$table.length) return;

            // Activate all history cells for keyboard;  TODO:  add "aria-controls"
            $table.find('.bc-has-history').each(function() {
                var historyId = 'bc-history-' + (++historyCount);
                var $td = $(this);

                $td.attr({
                    tabIndex: 0,
                    'aria-expanded': false,
                    'aria-controls': historyId
                });
                $td.find('.bc-history').attr('id', historyId);
                $td.find('.bc-history-link').attr('tabIndex', -1);
            });

            // Listen for clicks on "history" cells
            $table.on('click', '.bc-has-history, .bc-history', function() {
                var $target = $(this);

                if($target.hasClass('.bc-history')) {
                    e.stopImmediatePropagation();
                }
                closeAndOpenHistory($target);
            });

            // Listen for clicks on "close" buttons
            $table.on('click', '.bc-history-button', function(ev) {
                ev.stopImmediatePropagation();
                hideHistory();
            });

            // Listen for keys to open/close history
            $table.on('keypress', '.bc-has-history', function(ev) {
                var $td = $(this);
                var key = ev.which;

                if(key === 13 || key === 32) {
                    closeAndOpenHistory($td);
                    ev.preventDefault();
                } else if(key === 27) {
                    hideHistory();
                    ev.preventDefault();
                }
            });

            // Function which closes any open history, opens the target history
            function closeAndOpenHistory($td) {
                var previousOpenCell = $openCell && $openCell.get(0);

                // Close what's open, if anything
                hideHistory();

                // If they clicks the same cell (are closing), we can leave now
                if($td.get(0) == previousOpenCell) {
                    $openCell = false;
                    return;
                }

                // Set this cell as open
                $openCell = $td;

                // Show!
                showHistory();
            }

            // Opens the history for a given item
            function showHistory() {

                var $row = $openCell.closest('tr');
                var $history = $openCell.find('.bc-history').outerWidth($table.width());

                // get cell coords
                var cellLeft = $openCell.offset().left;

                // get cell left border
                var cellLeftBorder = $openCell.css('border-left-width');

                // get table coords
                var tableLeft = $table.offset().left;

                // left coord of table minus left coord of cell
                var historyLeft = tableLeft - cellLeft - parseInt(cellLeftBorder, 10) - 1;

                // top
                // can't just to top:100% in CSS because IE messes it up.

                // get cell height
                var cellTop = $openCell.outerHeight();

                // get cell top border
                var celltopBorder = $openCell.css('border-top-width');

                // get cell bottom border
                var cellBottomBorder = $openCell.css('border-bottom-width');

                var historyTop = cellTop - parseInt(cellBottomBorder, 10) - parseInt(celltopBorder, 10);

                var historyHeight;
                var windowWidth;

                // Add a close button if one doesn't already exist
                if($history.find('.bc-history-button').length === 0) {
                    $historyCloseButton.clone().appendTo($history);
                }

                // move history where it will display
                $history.css({
                    left: historyLeft,
                    top: historyTop
                });

                // measure height
                $history.css('display', 'block');
                $history.attr('aria-hidden', false);

                historyHeight = $history.outerHeight();

                // set max-height to 0 and visibility to visible
                $openCell.addClass('active');
                $openCell.attr('aria-expanded', true);

                setTimeout(function() {
                    $history.css('height', historyHeight);

                    // add measured height to history and to the cell/row it is being displayed beneath (CSS handles transition)
                    windowWidth = window.innerWidth;
                    if(windowWidth > 801) {
                        $row.find('th, td').css('border-bottom', historyHeight + 'px solid transparent');
                    } if(windowWidth > 481) {
                        $row.find('td').css('border-bottom', historyHeight + 'px solid transparent');
                    } else {
                        $openCell.css('border-bottom', historyHeight + 'px solid transparent');
                    }
                }, 10);
            }

            // Hides the history dropdown for a given cell
            function hideHistory(){
                if(!$openCell) return;

                var $history;

                $openCell.css('border-bottom', '').attr('aria-expanded', false);
                $openCell.closest('tr').find('th, td').css('border-bottom', '');

                $history = $openCell.find('.bc-history');
                $history.css('height', '').attr('aria-hidden', true);

                // if the focus is inside the .bc-history and we'd lose our keyboard place, move focus to parent
                if($.contains($openCell.get(0), document.activeElement)) {
                    $openCell.focus();
                }

                $openCell.removeClass('active');
                $history.css('display', 'none');
            }
        });
    };

})(jQuery);
<|endoftext|>