<commit_before>app.controller('AdminEditCollectionController', function($q, $routeParams, $scope, $timeout, $translate, AdminEdit,
    bulk, library, libraryAction) {

    $scope.section = 'collections';
    $scope.id = $routeParams.id;
    $scope.linked = $scope.id == 'link';

    $scope.graphFetched = false;
    $scope.graphData = {};
    $scope.hasTemplate = false;

    // Define helper functions
    function fetchGraphs() {
        var graphQuery = [],
            attrsQuery = [];

        $scope.graphFetched = false;

        angular.forEach($scope.item.entries, function(entry) {
            graphQuery.push({
                endpoint: 'library/graphs/' + entry.id,
                method: 'GET',
                params: {fields: 'id,name,options,template'}
            });
        });

        bulk.exec(graphQuery, function(data) {
            var graphData = {};

            angular.forEach(data, function(entry) {
                if (entry.status == 200) {
                    graphData[entry.data.id] = entry.data;

                    if (entry.data.template) {
                        attrsQuery.push({
                            endpoint: 'library/parse',
                            method: 'POST',
                            data: {id: entry.data.id, type: 'graphs'}
                        });
                    }

                    delete entry.data.id;
                }
            });

            $scope.graphFetched = true;

            if (attrsQuery.length > 0) {
                bulk.exec(attrsQuery, function(data) {
                    angular.forEach(data, function(entry, idx) {
                        graphData[attrsQuery[idx].data.id].templateKeys = entry.data;
                    });
                });
            }

            $scope.graphData = graphData;
        });
    }

    function updateTemplate() {
        if ($scope.linked) {
            libraryAction.parse({id: $scope.item.link, type: 'collections'}, function(data) {
                if (!$scope.item.attributes) {
                    $scope.item.attributes = {};
                }

                $scope.templateKeys = data;
            });
        } else {
            var keys = [],
                entries = [];

            // Parse templatable fields for attribute names
            entries.push($scope.item.description);
            if ($scope.item.options) {
                entries.push($scope.item.options.title);
            }

            angular.forEach(entries, function(input) {
                if (input) {
                    keys = keys.concat(input.matchAll(templateRegexp));
                }
            });

            // Parse graphs for attribute fields
            angular.forEach($scope.item.entries, function(entry) {
                angular.forEach(entry.attributes, function(attr) {
                    keys = keys.concat(attr.matchAll(templateRegexp));
                });
            });

            // Prepare attributes object and keys list
            keys.sort();
            keys = jQuery.unique(keys);

            $scope.templateKeys = keys;
            $scope.item.template = keys.length > 0;
        }
    }

    function setDefaults(input) {
        return angular.extend({
            options: {
                grid_size: 1
            }
        }, input);
    }

    // Define scope functions
    $scope.cancel = function(force) { AdminEdit.cancel($scope, force); };
    $scope.reset = function() { AdminEdit.reset($scope); fetchGraphs(); };

    $scope.save = function() {
        AdminEdit.save($scope, function(data) {
            // Remove empty data from attributes
            angular.forEach(data.entries, function(entry) {
                angular.forEach(entry.attributes, function(value, key) {
                    if (!value) {
                        delete entry.attributes[key];
                    }
                });

                if (entry.options) {
                    if (entry.options.constants) {
                        entry.options.constants = parseFloatList(entry.options.constants);
                    }

                    if (entry.options.percentiles) {
                        entry.options.percentiles = parseFloatList(entry.options.percentiles);
                    }
                }
            });

            angular.forEach(data.attributes, function(value, key) {
                if (!value) {
                    delete data.attributes[key];
                }
            });
        }, function(item) {
            if ($scope.linked && !item.link) {
                return false;
            }

            return true;
        });
    };

    $scope.remove = function(list, entry) {
        AdminEdit.remove($scope, list, entry);

        // Update template status
        updateTemplate();
    };

    $scope.selectGraph = function(data) {
        if (!data || !data.originalObject) {
            return;
        }

        angular.extend($scope.graph, data.originalObject);
    };

    $scope.setGraph = function() {
        if (!$scope.item.entries) {
            $scope.item.entries = [];
        }

        if (!$scope.graphData[$scope.graph.id]) {
            var id = $scope.graph.id;

            $scope.graphData[id] = angular.copy($scope.graph);

            libraryAction.parse({id: id, type: 'graphs'}, function(data) {
                $scope.graphData[id].templateKeys = data;
            });
        }

        var graph = {
            id: $scope.graph.id,
            name: $scope.graph.name
        };

        if ($scope.graph.index !== undefined) {
            angular.extend($scope.item.entries[$scope.graph.index], graph);
        } else {
            $scope.item.entries.push(graph);
        }

        // Update template status
        updateTemplate();

        $scope.resetGraph();
    };

    $scope.resetGraph = function() {
        $scope.graph = {};

        $scope.$broadcast('angucomplete-alt:clearInput', 'graph');
        $scope.$applyAsync(function() { angular.element('#graph_value').focus(); }, 0);
    };

    $scope.editGraph = function(entry) {
        var idx = $scope.item.entries.indexOf(entry);
        if (idx == -1) {
            return;
        }

        $scope.graph = angular.extend({index: idx}, $scope.graphData[entry.id]);

        $scope.$broadcast('angucomplete-alt:changeInput', 'graph', $scope.graphData[entry.id].name);

        $scope.$applyAsync(function() { angular.element('#graph_value').select(); });
    };

    $scope.toggleGraph = function(entry) {
        if (!entry.options) {
            entry.options = {enabled: false};
        } else {
            entry.options.enabled = !entry.options.enabled;
        }
    };

    $scope.editOptions = function(entry) {
        if (entry === null) {
            $scope.optionsEdit = null;
            $scope.entryOptions = null;
            return;
        }

        $scope.entryOptions = angular.copy(entry.options || {});
        $scope.optionsEdit = entry;
    };

    $scope.setOptions = function() {
        var idx = $scope.item.entries.indexOf($scope.optionsEdit);
        if (idx == -1) {
            return;
        }

        $scope.item.entries[idx].options = angular.extend($scope.item.entries[idx].options || {}, $scope.entryOptions);
        $scope.editOptions(null);
    };

    $scope.editAttrs = function(entry, main) {
        if (entry === null) {
            $scope.attrsEdit = null;
            $scope.entryAttrs = null;
            return;
        }

        if (main) {
            $scope.entryAttrs = angular.copy($scope.item.attributes || {});

            angular.forEach($scope.graphData, function(entry) {
                angular.forEach(entry.templateKeys, function(key) {
                    if (!$scope.entryAttrs[key]) {
                        $scope.entryAttrs[key] = '';
                    }
                });
            });
        } else {
            $scope.entryAttrs = angular.copy(entry.attributes || {});

            angular.forEach($scope.graphData[entry.id].templateKeys, function(key) {
                if (!$scope.entryAttrs[key]) {
                    $scope.entryAttrs[key] = '';
                }
            });
        }

        $scope.attrsEdit = entry;

        // Select first field
        $timeout(function() { angular.element('.pane .keylist .value:first :input').select(); }, 0);
    };

    $scope.setAttrs = function(main) {
        if (main) {
            $scope.item.attributes = angular.copy($scope.entryAttrs);
        } else {
            var idx = $scope.item.entries.indexOf($scope.attrsEdit);
            if (idx == -1) {
                return;
            }

            $scope.item.entries[idx].attributes = angular.extend($scope.item.entries[idx].attributes || {},
                $scope.entryAttrs);
        }

        $scope.editAttrs(null, main);
    };

    $scope.selectParent = function(data) {
        if (!data || !data.originalObject || !data.originalObject.id) {
            return;
        }

        $scope.item.parent = data.originalObject.id;
    };

    $scope.removeParent = function() {
        $scope.item.parent = null;
        $scope.$broadcast('angucomplete-alt:clearInput', 'parent');
    };

    $scope.selectTemplate = function(data) {
        if (!data || !data.originalObject || !data.originalObject.id) {
            return;
        }

        $scope.item.link = data.originalObject.id;
    };

    // Register watchers
    AdminEdit.watch($scope, function(newValue, oldValue) {
        if ($scope.step == 2 && !$scope.linked) {
            updateTemplate();
        } else if ($scope.linked) {
            if (!oldValue || newValue.link !== oldValue.link) {
                library.get({
                    type: 'collections',
                    id: newValue.link,
                    fields: 'name'
                }, function(data) {
                    // Restore selected template name
                    if (!oldValue) {
                        $scope.$broadcast('angucomplete-alt:changeInput', 'template', data.name);
                    }

                    updateTemplate();
                });
            }
        }
    });

    $scope.$watch('graphData', function(newValue, oldValue) {
        if (newValue === oldValue) {
            return;
        }

        var hasTemplate = false;
        angular.forEach(newValue, function(entry) {
            if (entry.template) {
                hasTemplate = true;
            }
        });

        if (hasTemplate !== $scope.hasTemplate) {
            $scope.hasTemplate = hasTemplate;
        }
    }, true);

    // Initialize scope
    AdminEdit.load($scope, function() {
        if ($scope.item.link) {
            $scope.linked = true;
        }

        if (!$scope.linked) {
            $scope.item = setDefaults($scope.item);
        }
        $scope.itemRef = angular.copy($scope.item);

        if (!$scope.linked) {
            $scope.selectedOptions = {};

            $scope.$watch('selectedOptions', function(newValue, oldValue) {
                // Handle select value changes
                if (!angular.equals(newValue, oldValue)) {
                    angular.forEach(newValue, function(entry, key) {
                        $scope.item.options[key] = entry.value;
                    });
                }
            }, true);

            $scope.graphsList = function(term) {
                return library.list({
                    type: 'graphs',
                    fields: 'id,name,options,template',
                    filter: 'glob:*' + term + '*'
                }).$promise;
            };

            $translate(['label.page_grid_1', 'label.page_grid_2', 'label.page_grid_3']).then(function(data) {
                var gridSizes = [];
                for (var i = 1; i <= 3; i++) {
                    gridSizes.push({name: data['label.page_grid_' + i], value: i});
                }
                $scope.collectionsGridSizes = gridSizes;

                applyOptions($scope.collectionsGridSizes, 'grid_size');
            });

            $scope.resetGraph();

            // Restore or set main options
            var applyOptions = function(list, key) {
                angular.forEach(list, function(entry) {
                    if (entry.value === $scope.item.options[key]) {
                        $scope.selectedOptions[key] = entry;
                    }
                });

                if (!$scope.selectedOptions[key]) {
                    $scope.selectedOptions[key] = list[0];
                }
            };

            // Trigger initial graph information retrieval
            fetchGraphs();
        } else {
            $scope.templateSources = function(term) {
                return library.list({type: 'collections', kind: 'template', fields: 'id,name',
                    filter: 'glob:*' + term + '*'}).$promise;
            };

            // Select first field
            $scope.$applyAsync(function() { angular.element('.pane :input:visible:first').select(); });
        }

        $scope.collectionsList = function(term) {
            var defer = $q.defer();

            library.list({
                type: 'collections',
                fields: 'id,name,parent',
                filter: 'glob:*' + term + '*'
            }).$promise.then(function(data) {
                var collections = {},
                    assocs = {};

                angular.forEach(data, function(entry) {
                    // Set parent association
                    if (entry.parent) {
                        if (!assocs[entry.parent]) {
                            assocs[entry.parent] = [];
                        }
                        assocs[entry.parent].push(entry.id);
                    }

                    // Set result entry
                    collections[entry.id] = entry;
                });

                // Clean up list from current children collections
                var stack = [$scope.id],
                    cur = null;

                while (stack.length > 0) {
                    cur = stack.shift();
                    if (assocs[cur]) {
                        stack = stack.concat(assocs[cur]);
                    }
                    delete collections[cur];
                }

                // Return cleaned up list
                var result = [];

                angular.forEach(collections, function(entry) {
                    result.push(entry);
                });

                defer.resolve(result);
            });

            return defer.promise;
        };

        // Resolve parent name
        if ($scope.item.parent) {
            library.get({
                type: 'collections',
                id: $scope.item.parent,
                fields: 'name'
            }, function(data) {
                $scope.$broadcast('angucomplete-alt:changeInput', 'parent', data.name);
            });
        }
    });
});
<commit_msg>Prevent templates display in parent completion<commit_after>app.controller('AdminEditCollectionController', function($q, $routeParams, $scope, $timeout, $translate, AdminEdit,
    bulk, library, libraryAction) {

    $scope.section = 'collections';
    $scope.id = $routeParams.id;
    $scope.linked = $scope.id == 'link';

    $scope.graphFetched = false;
    $scope.graphData = {};
    $scope.hasTemplate = false;

    // Define helper functions
    function fetchGraphs() {
        var graphQuery = [],
            attrsQuery = [];

        $scope.graphFetched = false;

        angular.forEach($scope.item.entries, function(entry) {
            graphQuery.push({
                endpoint: 'library/graphs/' + entry.id,
                method: 'GET',
                params: {fields: 'id,name,options,template'}
            });
        });

        bulk.exec(graphQuery, function(data) {
            var graphData = {};

            angular.forEach(data, function(entry) {
                if (entry.status == 200) {
                    graphData[entry.data.id] = entry.data;

                    if (entry.data.template) {
                        attrsQuery.push({
                            endpoint: 'library/parse',
                            method: 'POST',
                            data: {id: entry.data.id, type: 'graphs'}
                        });
                    }

                    delete entry.data.id;
                }
            });

            $scope.graphFetched = true;

            if (attrsQuery.length > 0) {
                bulk.exec(attrsQuery, function(data) {
                    angular.forEach(data, function(entry, idx) {
                        graphData[attrsQuery[idx].data.id].templateKeys = entry.data;
                    });
                });
            }

            $scope.graphData = graphData;
        });
    }

    function updateTemplate() {
        if ($scope.linked) {
            libraryAction.parse({id: $scope.item.link, type: 'collections'}, function(data) {
                if (!$scope.item.attributes) {
                    $scope.item.attributes = {};
                }

                $scope.templateKeys = data;
            });
        } else {
            var keys = [],
                entries = [];

            // Parse templatable fields for attribute names
            entries.push($scope.item.description);
            if ($scope.item.options) {
                entries.push($scope.item.options.title);
            }

            angular.forEach(entries, function(input) {
                if (input) {
                    keys = keys.concat(input.matchAll(templateRegexp));
                }
            });

            // Parse graphs for attribute fields
            angular.forEach($scope.item.entries, function(entry) {
                angular.forEach(entry.attributes, function(attr) {
                    keys = keys.concat(attr.matchAll(templateRegexp));
                });
            });

            // Prepare attributes object and keys list
            keys.sort();
            keys = jQuery.unique(keys);

            $scope.templateKeys = keys;
            $scope.item.template = keys.length > 0;
        }
    }

    function setDefaults(input) {
        return angular.extend({
            options: {
                grid_size: 1
            }
        }, input);
    }

    // Define scope functions
    $scope.cancel = function(force) { AdminEdit.cancel($scope, force); };
    $scope.reset = function() { AdminEdit.reset($scope); fetchGraphs(); };

    $scope.save = function() {
        AdminEdit.save($scope, function(data) {
            // Remove empty data from attributes
            angular.forEach(data.entries, function(entry) {
                angular.forEach(entry.attributes, function(value, key) {
                    if (!value) {
                        delete entry.attributes[key];
                    }
                });

                if (entry.options) {
                    if (entry.options.constants) {
                        entry.options.constants = parseFloatList(entry.options.constants);
                    }

                    if (entry.options.percentiles) {
                        entry.options.percentiles = parseFloatList(entry.options.percentiles);
                    }
                }
            });

            angular.forEach(data.attributes, function(value, key) {
                if (!value) {
                    delete data.attributes[key];
                }
            });
        }, function(item) {
            if ($scope.linked && !item.link) {
                return false;
            }

            return true;
        });
    };

    $scope.remove = function(list, entry) {
        AdminEdit.remove($scope, list, entry);

        // Update template status
        updateTemplate();
    };

    $scope.selectGraph = function(data) {
        if (!data || !data.originalObject) {
            return;
        }

        angular.extend($scope.graph, data.originalObject);
    };

    $scope.setGraph = function() {
        if (!$scope.item.entries) {
            $scope.item.entries = [];
        }

        if (!$scope.graphData[$scope.graph.id]) {
            var id = $scope.graph.id;

            $scope.graphData[id] = angular.copy($scope.graph);

            libraryAction.parse({id: id, type: 'graphs'}, function(data) {
                $scope.graphData[id].templateKeys = data;
            });
        }

        var graph = {
            id: $scope.graph.id,
            name: $scope.graph.name
        };

        if ($scope.graph.index !== undefined) {
            angular.extend($scope.item.entries[$scope.graph.index], graph);
        } else {
            $scope.item.entries.push(graph);
        }

        // Update template status
        updateTemplate();

        $scope.resetGraph();
    };

    $scope.resetGraph = function() {
        $scope.graph = {};

        $scope.$broadcast('angucomplete-alt:clearInput', 'graph');
        $scope.$applyAsync(function() { angular.element('#graph_value').focus(); }, 0);
    };

    $scope.editGraph = function(entry) {
        var idx = $scope.item.entries.indexOf(entry);
        if (idx == -1) {
            return;
        }

        $scope.graph = angular.extend({index: idx}, $scope.graphData[entry.id]);

        $scope.$broadcast('angucomplete-alt:changeInput', 'graph', $scope.graphData[entry.id].name);

        $scope.$applyAsync(function() { angular.element('#graph_value').select(); });
    };

    $scope.toggleGraph = function(entry) {
        if (!entry.options) {
            entry.options = {enabled: false};
        } else {
            entry.options.enabled = !entry.options.enabled;
        }
    };

    $scope.editOptions = function(entry) {
        if (entry === null) {
            $scope.optionsEdit = null;
            $scope.entryOptions = null;
            return;
        }

        $scope.entryOptions = angular.copy(entry.options || {});
        $scope.optionsEdit = entry;
    };

    $scope.setOptions = function() {
        var idx = $scope.item.entries.indexOf($scope.optionsEdit);
        if (idx == -1) {
            return;
        }

        $scope.item.entries[idx].options = angular.extend($scope.item.entries[idx].options || {}, $scope.entryOptions);
        $scope.editOptions(null);
    };

    $scope.editAttrs = function(entry, main) {
        if (entry === null) {
            $scope.attrsEdit = null;
            $scope.entryAttrs = null;
            return;
        }

        if (main) {
            $scope.entryAttrs = angular.copy($scope.item.attributes || {});

            angular.forEach($scope.graphData, function(entry) {
                angular.forEach(entry.templateKeys, function(key) {
                    if (!$scope.entryAttrs[key]) {
                        $scope.entryAttrs[key] = '';
                    }
                });
            });
        } else {
            $scope.entryAttrs = angular.copy(entry.attributes || {});

            angular.forEach($scope.graphData[entry.id].templateKeys, function(key) {
                if (!$scope.entryAttrs[key]) {
                    $scope.entryAttrs[key] = '';
                }
            });
        }

        $scope.attrsEdit = entry;

        // Select first field
        $timeout(function() { angular.element('.pane .keylist .value:first :input').select(); }, 0);
    };

    $scope.setAttrs = function(main) {
        if (main) {
            $scope.item.attributes = angular.copy($scope.entryAttrs);
        } else {
            var idx = $scope.item.entries.indexOf($scope.attrsEdit);
            if (idx == -1) {
                return;
            }

            $scope.item.entries[idx].attributes = angular.extend($scope.item.entries[idx].attributes || {},
                $scope.entryAttrs);
        }

        $scope.editAttrs(null, main);
    };

    $scope.selectParent = function(data) {
        if (!data || !data.originalObject || !data.originalObject.id) {
            return;
        }

        $scope.item.parent = data.originalObject.id;
    };

    $scope.removeParent = function() {
        $scope.item.parent = null;
        $scope.$broadcast('angucomplete-alt:clearInput', 'parent');
    };

    $scope.selectTemplate = function(data) {
        if (!data || !data.originalObject || !data.originalObject.id) {
            return;
        }

        $scope.item.link = data.originalObject.id;
    };

    // Register watchers
    AdminEdit.watch($scope, function(newValue, oldValue) {
        if ($scope.step == 2 && !$scope.linked) {
            updateTemplate();
        } else if ($scope.linked) {
            if (!oldValue || newValue.link !== oldValue.link) {
                library.get({
                    type: 'collections',
                    id: newValue.link,
                    fields: 'name'
                }, function(data) {
                    // Restore selected template name
                    if (!oldValue) {
                        $scope.$broadcast('angucomplete-alt:changeInput', 'template', data.name);
                    }

                    updateTemplate();
                });
            }
        }
    });

    $scope.$watch('graphData', function(newValue, oldValue) {
        if (newValue === oldValue) {
            return;
        }

        var hasTemplate = false;
        angular.forEach(newValue, function(entry) {
            if (entry.template) {
                hasTemplate = true;
            }
        });

        if (hasTemplate !== $scope.hasTemplate) {
            $scope.hasTemplate = hasTemplate;
        }
    }, true);

    // Initialize scope
    AdminEdit.load($scope, function() {
        if ($scope.item.link) {
            $scope.linked = true;
        }

        if (!$scope.linked) {
            $scope.item = setDefaults($scope.item);
        }
        $scope.itemRef = angular.copy($scope.item);

        if (!$scope.linked) {
            $scope.selectedOptions = {};

            $scope.$watch('selectedOptions', function(newValue, oldValue) {
                // Handle select value changes
                if (!angular.equals(newValue, oldValue)) {
                    angular.forEach(newValue, function(entry, key) {
                        $scope.item.options[key] = entry.value;
                    });
                }
            }, true);

            $scope.graphsList = function(term) {
                return library.list({
                    type: 'graphs',
                    fields: 'id,name,options,template',
                    filter: 'glob:*' + term + '*'
                }).$promise;
            };

            $translate(['label.page_grid_1', 'label.page_grid_2', 'label.page_grid_3']).then(function(data) {
                var gridSizes = [];
                for (var i = 1; i <= 3; i++) {
                    gridSizes.push({name: data['label.page_grid_' + i], value: i});
                }
                $scope.collectionsGridSizes = gridSizes;

                applyOptions($scope.collectionsGridSizes, 'grid_size');
            });

            $scope.resetGraph();

            // Restore or set main options
            var applyOptions = function(list, key) {
                angular.forEach(list, function(entry) {
                    if (entry.value === $scope.item.options[key]) {
                        $scope.selectedOptions[key] = entry;
                    }
                });

                if (!$scope.selectedOptions[key]) {
                    $scope.selectedOptions[key] = list[0];
                }
            };

            // Trigger initial graph information retrieval
            fetchGraphs();
        } else {
            $scope.templateSources = function(term) {
                return library.list({type: 'collections', kind: 'template', fields: 'id,name',
                    filter: 'glob:*' + term + '*'}).$promise;
            };

            // Select first field
            $scope.$applyAsync(function() { angular.element('.pane :input:visible:first').select(); });
        }

        $scope.collectionsList = function(term) {
            var defer = $q.defer();

            library.list({
                type: 'collections',
                kind: 'raw',
                fields: 'id,name,parent',
                filter: 'glob:*' + term + '*'
            }).$promise.then(function(data) {
                var collections = {},
                    assocs = {};

                angular.forEach(data, function(entry) {
                    // Set parent association
                    if (entry.parent) {
                        if (!assocs[entry.parent]) {
                            assocs[entry.parent] = [];
                        }
                        assocs[entry.parent].push(entry.id);
                    }

                    // Set result entry
                    collections[entry.id] = entry;
                });

                // Clean up list from current children collections
                var stack = [$scope.id],
                    cur = null;

                while (stack.length > 0) {
                    cur = stack.shift();
                    if (assocs[cur]) {
                        stack = stack.concat(assocs[cur]);
                    }
                    delete collections[cur];
                }

                // Return cleaned up list
                var result = [];

                angular.forEach(collections, function(entry) {
                    result.push(entry);
                });

                defer.resolve(result);
            });

            return defer.promise;
        };

        // Resolve parent name
        if ($scope.item.parent) {
            library.get({
                type: 'collections',
                id: $scope.item.parent,
                fields: 'name'
            }, function(data) {
                $scope.$broadcast('angucomplete-alt:changeInput', 'parent', data.name);
            });
        }
    });
});
<|endoftext|>


<commit_before>/* global document beforeEach afterEach describe it event window */
import { expect } from 'chai';
import sinon from 'sinon';

const proxyquireStrict = require('proxyquire').noCallThru();

describe('Toggle init', () => {
  const domSliderStub = {};
  domSliderStub.slideToggle = () => {};

  const {
    toggle,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': domSliderStub });

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll.getCall(0).args[0]).eql('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const domSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': domSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('a');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add('sprk-b-TypeBodyThree', 'sprk-b-Link', 'sprk-b-Link--standalone');

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--standalone',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add('sprk-b-TypeBodyTwo', 'sprk-c-Accordion__content');
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => {};

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).eql(true);
  });

  it('should add aria-expanded attribute set to false if not found on trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).eql('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).eql(true);
  });

  it('should toggle accordion item open', () => {
    contentAccordion.slideToggle = () => new Promise((resolve) => { resolve('cats'); });
    handleToggleClick(contentAccordion, iconAccordion, iconAccordionUseElement, triggerAccordion);
    expect(containerAccordion.classList.contains('sprk-c-Accordion__item--open')).eql(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).eql('#chevron-down-circle-filled');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(iconAccordion, iconAccordionUseElement, 'chevron-down-circle', 'chevron-down-circle-filled');
    expect(iconAccordionUseElement.getAttribute('xlink:href')).eql('#chevron-down-circle-filled');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).eql('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).eql('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList.contains('sprk-c-Accordion__item--open')).eql(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).eql('none');
    }, 100);
  });

  it('should enable clicks after slide toggle animation runs', () => {
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).eql('auto');
    }, 300);
  });
});
<commit_msg>bring all toggle tests to 100%<commit_after>/* global document beforeEach afterEach describe it event window */
import { expect } from 'chai';
import sinon from 'sinon';

const proxyquireStrict = require('proxyquire').noCallThru();

describe('Toggle init', () => {
  const domSliderStub = {};
  domSliderStub.slideToggle = () => {};

  const {
    toggle,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': domSliderStub });

  afterEach(() => {
    document.querySelectorAll.restore();
  });

  it('should call getElements once with the correct selector', () => {
    sinon.spy(document, 'querySelectorAll');
    toggle();
    expect(document.querySelectorAll.getCall(0).args[0]).eql('[data-sprk-toggle="container"]');
  });
});

describe('Toggle tests', () => {
  let container;
  let trigger;
  let triggerAccordion;
  let content;
  let contentAccordion;
  let icon;
  let containerAccordion;
  let iconAccordion;
  let iconAccordionUseElement;
  const domSliderStub = {};

  const {
    toggleIconType,
    toggleAriaExpanded,
    handleToggleClick,
    bindToggleUIEvents,
  } = proxyquireStrict('../components/toggle', { 'dom-slider': domSliderStub });

  beforeEach(() => {
    container = document.createElement('div');
    container.setAttribute('data-sprk-toggle', 'container');

    containerAccordion = document.createElement('li');
    containerAccordion.setAttribute('data-sprk-toggle', 'container');
    containerAccordion.classList.add('sprk-c-Accordion__item');

    trigger = document.createElement('a');
    trigger.setAttribute('data-sprk-toggle', 'trigger');
    trigger.setAttribute('aria-expanded', 'false');
    trigger.textContent = 'My Toggle Link Text';
    trigger.classList.add('sprk-b-TypeBodyThree', 'sprk-b-Link', 'sprk-b-Link--standalone');

    triggerAccordion = document.createElement('a');
    triggerAccordion.setAttribute('data-sprk-toggle', 'trigger');
    triggerAccordion.setAttribute('data-sprk-toggle-type', 'accordion');
    triggerAccordion.setAttribute('aria-expanded', 'false');
    triggerAccordion.textContent = 'My Toggle Accordion Link Text';
    triggerAccordion.classList.add(
      'sprk-b-TypeBodyThree',
      'sprk-b-Link',
      'sprk-b-Link--standalone',
    );

    content = document.createElement('div');
    content.setAttribute('data-sprk-toggle', 'content');
    content.textContent = 'This is the toggle content..';
    content.classList.add('sprk-b-TypeBodyFour', 'sprk-u-pts');

    contentAccordion = document.createElement('p');
    contentAccordion.setAttribute('data-sprk-toggle', 'content');
    contentAccordion.textContent = 'This is the toggle accordion content.';
    contentAccordion.classList.add('sprk-b-TypeBodyTwo', 'sprk-c-Accordion__content');
    contentAccordion.style.display = 'none';
    contentAccordion.slideToggle = () => {};

    icon = document.createElement('svg');
    icon.setAttribute('data-sprk-toggle', 'icon');

    iconAccordion = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    iconAccordion.classList.add('sprk-c-Icon');
    iconAccordion.setAttribute('data-sprk-toggle', 'icon');

    iconAccordionUseElement = document.createElement('use');
    iconAccordionUseElement.setAttribute('xlink:href', '#chevron-down');
    iconAccordionUseElement.setAttribute('data-sprk-toggle', 'svgUse');
    iconAccordion.appendChild(iconAccordionUseElement);

    trigger.append(icon);
    container.append(trigger);
    container.append(content);

    triggerAccordion.append(iconAccordion);
    containerAccordion.append(triggerAccordion);
    containerAccordion.append(contentAccordion);
  });

  it('should toggle aria-expanded attribute for toggles', () => {
    toggleAriaExpanded(trigger);
    expect(trigger.hasAttribute('aria-expanded')).eql(true);
  });

  it('should add aria-expanded attribute set to false if not found on trigger', () => {
    triggerAccordion.removeAttribute('aria-expanded');
    toggleAriaExpanded(triggerAccordion);
    expect(triggerAccordion.getAttribute('aria-expanded')).eql('false');
  });

  it('should toggle aria-expanded attribute to false', () => {
    toggleAriaExpanded(trigger); // Is true, makes it false
    toggleAriaExpanded(trigger); // Is false, makes it true
    expect(trigger.hasAttribute('aria-expanded')).eql(true);
  });

  it('should toggle accordion item open', () => {
    contentAccordion.slideToggle = () => new Promise((resolve) => { resolve('tests'); });
    handleToggleClick(contentAccordion, iconAccordion, iconAccordionUseElement, triggerAccordion);
    expect(containerAccordion.classList.contains('sprk-c-Accordion__item--open')).eql(true);
    expect(iconAccordionUseElement.getAttribute('xlink:href')).eql('#chevron-down-circle-filled');
  });

  it('should toggle accordion icon open', () => {
    toggleIconType(iconAccordion, iconAccordionUseElement, 'chevron-down-circle', 'chevron-down-circle-filled');
    expect(iconAccordionUseElement.getAttribute('xlink:href')).eql('#chevron-down-circle-filled');
  });

  it('should add listener to toggle trigger', () => {
    sinon.spy(trigger, 'addEventListener');
    bindToggleUIEvents(container);
    expect(trigger.addEventListener.getCall(0).args[0]).eql('click');
  });

  it('should add listener to Accordion toggle trigger', () => {
    sinon.spy(triggerAccordion, 'addEventListener');
    bindToggleUIEvents(containerAccordion);
    expect(triggerAccordion.addEventListener.getCall(0).args[0]).eql('click');
  });

  it('should show details when clicked', () => {
    bindToggleUIEvents(containerAccordion);
    const event = new window.Event('click');
    triggerAccordion.dispatchEvent(event);
    expect(containerAccordion.classList.contains('sprk-c-Accordion__item--open')).eql(true);
  });

  it('should disable clicks until after slide toggle animation runs', () => {
    bindToggleUIEvents(container);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).eql('none');
    }, 100);
  });

  it('should enable clicks after slide toggle animation runs', () => {
    const event = new window.Event('click');
    trigger.dispatchEvent(event);
    setTimeout(() => {
      expect(trigger.style.pointerEvents).eql('auto');
    }, 300);
  });

  it('should still toggle content if icon is not present', () => {
    contentAccordion.slideToggle = () => new Promise((resolve) => { resolve('test'); });
    handleToggleClick(contentAccordion, null, null, triggerAccordion);
    expect(containerAccordion.classList.contains('sprk-c-Accordion__item--open')).eql(true);
  });

  it('should not add Accordion__item class if toggle is not an accordion', () => {
    content.slideToggle = () => new Promise((resolve) => { resolve('test'); });
    handleToggleClick(content, null, null, trigger);
    expect(container.classList.contains('sprk-c-Accordion__item--open')).eql(false);
  });
});
<|endoftext|>


<commit_before>/*
 * Copyright 2013 The Toolkitchen Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file.
 */

/**
 * Implements `document.register`
 * @module CustomElements
*/

/**
 * Polyfilled extensions to the `document` object.
 * @class Document
*/

(function() {

/**
 * Registers a custom tag name with the document.
 *
 * When a registered element is created, a `readyCallback` method is called
 * in the scope of the element. The `readyCallback` method can be specified on
 * either `inOptions.prototype` or `inOptions.lifecycle` with the latter taking
 * precedence.
 *
 * @method register
 * @param {String} inName The tag name to register. Must include a dash ('-'),
 *    for example 'x-component'.
 * @param {Object} inOptions
 *    @param {String} [inOptions.extends]
 *      (_off spec_) Tag name of an element to extend (or blank for a new
 *      element). This paramter is not part of the specification, but instead
 *      is a hint for the polyfill because the extendee is difficult to infer.
 *      Remember that the input prototype must chain to the extended element's
 *      prototype (or HTMLElement.prototype) regardless of the value of
 *      `extends`.
 *    @param {Object} inOptions.prototype The prototype to use for the new
 *      element. The prototype must inherit from HTMLElement.
 *    @param {Object} [inOptions.lifecycle]
 *      Callbacks that fire at important phases in the life of the custom
 *      element.
 *
 * @example
 *      FancyButton = document.register("fancy-button", {
 *        extends: 'button',
 *        prototype: Object.create(HTMLButtonElement.prototype, {
 *          readyCallback: {
 *            value: function() {
 *              console.log("a fancy-button was created",
 *            }
 *          }
 *        })
 *      });
 * @return {Function} Constructor for the newly registered type.
 */
function register(inName, inOptions) {
  //console.warn('document.register("' + inName + '", ', inOptions, ')');
  // construct a defintion out of options
  // TODO(sjmiles): probably should clone inOptions instead of mutating it
  var definition = inOptions || {};
  // record name
  definition.name = inName;
  // ensure a lifecycle object so we don't have to null test it
  definition.lifecycle = definition.lifecycle || {};
  // must have a prototype, default to an extension of HTMLElement
  // TODO(sjmiles): probably should throw if no prototype, check spec
  definition.prototype = definition.prototype
      || Object.create(HTMLUnknownElement.prototype);
  // build a list of ancestral custom elements (for lifecycle management)
  definition.ancestry = ancestry(definition.extends);
  // extensions of native specializations of HTMLElement require localName
  // to remain native, and use secondary 'is' specifier for extension type
  resolveTagName(definition);
  // some platforms require modifications to the user-supplied prototype
  // chain
  resolvePrototypeChain(definition);
  // 7.1.5: Register the DEFINITION with DOCUMENT
  registerDefinition(inName, definition);
  // 7.1.7. Run custom element constructor generation algorithm with PROTOTYPE
  // 7.1.8. Return the output of the previous step.
  definition.ctor = generateConstructor(definition);
  definition.ctor.prototype = definition.prototype;
  // blanket upgrade (?)
  document.upgradeElements();
  return definition.ctor;
}

function ancestry(inExtends) {
  var extendee = registry[inExtends];
  if (extendee) {
    return ancestry(extendee.extends).concat([extendee]);
  }
  return [];
}

function resolveTagName(inDefinition) {
  // if we are explicitly extending something, that thing is our
  // baseTag, unless it represents a custom component
  var baseTag = inDefinition.extends;
  // if our ancestry includes custom components, we only have a
  // baseTag if one of them does
  for (var i=0, a; (a=inDefinition.ancestry[i]); i++) {
    baseTag = a.is && a.tag;
  }
  // our tag is our baseTag, if it exists, and otherwise just our name
  inDefinition.tag = baseTag || inDefinition.name;
  if (baseTag) {
    // if there is a base tag, use secondary 'is' specifier
    inDefinition.is = inDefinition.name;
  }
}

function resolvePrototypeChain(inDefinition) {
  // TODO(sjmiles): ShadowDOM polyfill pollution
  // under ShadowDOM polyfill we need to use Wrapper prototype chain instead
  // of native DOM
  if (window.WrapperElement
        && !(inDefinition.prototype instanceof WrapperElement)) {
    if (inDefinition.is) {
      // for non-trivial extensions, work out both prototypes
      var inst = domCreateElement(inDefinition.tag);
      var native = Object.getPrototypeOf(unwrap(inst));
      var wrapperNative = Object.getPrototypeOf(inst);
    } else {
      // otherwise, use the defaults
      native = HTMLElement.prototype;
      wrapperNative = WrapperHTMLUnknownElement.prototype;
    }
    if (Object.__proto__) {
      // search and replace 'native' protoype with 'wrapperNative'
      var p = inDefinition.prototype, pp;
      while (true) {
        pp = Object.getPrototypeOf(p);
        if (pp === native || pp == HTMLUnknownElement.prototype) {
          break;
        }
        p = pp;
      }
      p.__proto__ = wrapperNative;
      //console.log(native, wrapperNative, p);
    } else {
      // TODO(sjmiles): support IE
    }
  }
}

// SECTION 4

function instantiate(inDefinition) {
  // 4.a.1. Create a new object that implements PROTOTYPE
  // 4.a.2. Let ELEMENT by this new object
  //
  // the custom element instantiation algorithm must also ensure that the
  // output is a valid DOM element with the proper wrapper in place.
  //
  return upgrade(domCreateElement(inDefinition.tag), inDefinition);
}

function upgrade(inElement, inDefinition) {
  // make 'element' implement inDefinition.prototype
  implement(inElement, inDefinition);
  // some definitions specify an 'is' attribute
  if (inDefinition.is) {
    inElement.setAttribute('is', inDefinition.is);
  }
  // flag as upgraded
  inElement.__upgraded__ = true;
  // invoke lifecycle.created callbacks
  created(inElement, inDefinition);
  // OUTPUT
  return inElement;
}

function implement(inElement, inDefinition) {
  // prototype swizzling is best
  if (Object.__proto__) {
    inElement.__proto__ = inDefinition.prototype;
  } else {
    // where above we can re-acquire inPrototype via
    // getPrototypeOf(Element), we cannot do so when
    // we use mixin, so we install a magic reference
    if (!Object.__proto__) {
      inElement.__proto__ = inDefinition.prototype;
    }
    mixin(inElement, inDefinition.prototype);
  }
}

/*
function mixinCustomPrototype(inTarget, inSrc, inNative) {
  console.group(inTarget.localName);
  var p = inSrc;
  while (p !== inNative) {
    console.group('proto');
    var keys = Object.keys(p);
    for (var i=0, k; k=keys[i]; i++) {
      console.log(k);
      Object.defineProperty(inTarget, k, Object.getOwnPropertyDescriptor(p, k));
    }
    console.groupEnd();
    p = Object.getPrototypeOf(p);
  }
  console.groupEnd();
}
*/

function created(inElement, inDefinition) {
  var readyCallback = inDefinition.lifecycle.readyCallback ||
      inElement.readyCallback;
  if (readyCallback) {
    readyCallback.call(inElement);
  }
}

var registry = {};
var registrySlctr = '';

function registerDefinition(inName, inDefinition) {
  registry[inName] = inDefinition;
  registrySlctr += (registrySlctr ? ',' : '');
  if (inDefinition.extends) {
    registrySlctr += inDefinition.tag + '[is=' + inDefinition.is + '],';
  }
  registrySlctr += inName;
}

function generateConstructor(inDefinition) {
  return function() {
    return instantiate(inDefinition);
  };
}

function createElement(inTag) {
  var definition = registry[inTag];
  if (definition) {
    return new definition.ctor();
  }
  return domCreateElement(inTag);
}

/**
 * Upgrade an element to a custom element. Upgrading an element
 * causes the custom prototype to be applied, an `is` attribute to be attached
 * (as needed), and invocation of the `readyCallback`.
 * `upgradeElement` does nothing is the element is already upgraded, or
 * if it matches no registered custom tag name.
 *
 * @method ugpradeElement
 * @param {Element} inElement The element to upgrade.
 * @return {Element} The upgraded element.
 */
function upgradeElement(inElement) {
  if (!inElement.__upgraded__) {
    var type = inElement.getAttribute('is') || inElement.localName;
    var definition = registry[type];
    return definition && upgrade(inElement, definition);
  }
}

/**
 * Upgrade all elements under `inRoot` that match selector `inSlctr`.
 * causes the custom prototype to be applied, an `is` attribute to be attached
 * (as needed), and invocation of the `readyCallback`.
 * `upgradeElement` does nothing is the element is already upgraded, or
 * if it matches no registered custom tag name.
 *
 * @method ugpradeElements
 * @param {Node} inRoot The root of the DOM subtree in which elements are to
 *  be upgraded.
 * @param {String} [inSlctr] An optional selector for matching specific
 * elements, otherwise all register element types are upgraded.
 */
function upgradeElements(inRoot, inSlctr) {
  var slctr = inSlctr || registrySlctr;
  if (slctr) {
    var root = inRoot || document;
    forEach(root.querySelectorAll(slctr), upgradeElement);
  }
}

// utilities

var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);

// copy all properties from inProps (et al) to inObj
function mixin(inObj/*, inProps, inMoreProps, ...*/) {
  var obj = inObj || {};
  for (var i = 1; i < arguments.length; i++) {
    var p = arguments[i];
    // TODO(sjmiles): on IE we are using mixin
    // to generate custom element instances, as we have
    // no way to alter element prototypes after creation
    // (nor a way to create an element with a custom prototype)
    // however, prototype sources (inSource) are ultimately
    // chained to a native prototype (HTMLElement or inheritor)
    // and trying to copy HTMLElement properties to itself throwss
    // in IE
    // we don't actually want to copy those properties anyway, but I
    // can't find a way to determine if a prototype is a native
    // or custom inheritor of HTMLElement
    // ad hoc solution is to simply stop at the first exception
    // an alternative exists if we have a tagName hint: then we can
    // work out where the native objects are in the prototype chain
    try {
      for (var n in p) {
        copyProperty(n, p, obj);
      }
    } catch(x) {
      //console.log(x);
    }
  }
  return obj;
}

// copy property inName from inSource object to inTarget object
function copyProperty(inName, inSource, inTarget) {
  var pd = getPropertyDescriptor(inSource, inName);
  Object.defineProperty(inTarget, inName, pd);
}

// get property descriptor for inName on inObject, even if
// inName exists on some link in inObject's prototype chain
function getPropertyDescriptor(inObject, inName) {
  if (inObject) {
    var pd = Object.getOwnPropertyDescriptor(inObject, inName);
    return pd || getPropertyDescriptor(Object.getPrototypeOf(inObject), inName);
  }
}

// capture native createElement before we override it
var domCreateElement = document.createElement.bind(document);

// exports

document.register = register;
document.upgradeElement = upgradeElement;
document.upgradeElements = upgradeElements;
document.createElement = createElement; // override

// TODO(sjmiles): temporary, control scope better
window.mixin = mixin;

})();
<commit_msg>use selective mixin on platforms without __proto__ (namely IE) for efficiency and to support wrapper environments (aka ShadowDOM polyfill) where the DOM prototypes may be custom<commit_after>/*
 * Copyright 2013 The Toolkitchen Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file.
 */

/**
 * Implements `document.register`
 * @module CustomElements
*/

/**
 * Polyfilled extensions to the `document` object.
 * @class Document
*/

(function() {

/**
 * Registers a custom tag name with the document.
 *
 * When a registered element is created, a `readyCallback` method is called
 * in the scope of the element. The `readyCallback` method can be specified on
 * either `inOptions.prototype` or `inOptions.lifecycle` with the latter taking
 * precedence.
 *
 * @method register
 * @param {String} inName The tag name to register. Must include a dash ('-'),
 *    for example 'x-component'.
 * @param {Object} inOptions
 *    @param {String} [inOptions.extends]
 *      (_off spec_) Tag name of an element to extend (or blank for a new
 *      element). This paramter is not part of the specification, but instead
 *      is a hint for the polyfill because the extendee is difficult to infer.
 *      Remember that the input prototype must chain to the extended element's
 *      prototype (or HTMLElement.prototype) regardless of the value of
 *      `extends`.
 *    @param {Object} inOptions.prototype The prototype to use for the new
 *      element. The prototype must inherit from HTMLElement.
 *    @param {Object} [inOptions.lifecycle]
 *      Callbacks that fire at important phases in the life of the custom
 *      element.
 *
 * @example
 *      FancyButton = document.register("fancy-button", {
 *        extends: 'button',
 *        prototype: Object.create(HTMLButtonElement.prototype, {
 *          readyCallback: {
 *            value: function() {
 *              console.log("a fancy-button was created",
 *            }
 *          }
 *        })
 *      });
 * @return {Function} Constructor for the newly registered type.
 */
function register(inName, inOptions) {
  //console.warn('document.register("' + inName + '", ', inOptions, ')');
  // construct a defintion out of options
  // TODO(sjmiles): probably should clone inOptions instead of mutating it
  var definition = inOptions || {};
  // record name
  definition.name = inName;
  // ensure a lifecycle object so we don't have to null test it
  definition.lifecycle = definition.lifecycle || {};
  // must have a prototype, default to an extension of HTMLElement
  // TODO(sjmiles): probably should throw if no prototype, check spec
  definition.prototype = definition.prototype
      || Object.create(HTMLUnknownElement.prototype);
  // build a list of ancestral custom elements (for lifecycle management)
  definition.ancestry = ancestry(definition.extends);
  // extensions of native specializations of HTMLElement require localName
  // to remain native, and use secondary 'is' specifier for extension type
  resolveTagName(definition);
  // some platforms require modifications to the user-supplied prototype
  // chain
  resolvePrototypeChain(definition);
  // 7.1.5: Register the DEFINITION with DOCUMENT
  registerDefinition(inName, definition);
  // 7.1.7. Run custom element constructor generation algorithm with PROTOTYPE
  // 7.1.8. Return the output of the previous step.
  definition.ctor = generateConstructor(definition);
  definition.ctor.prototype = definition.prototype;
  // blanket upgrade (?)
  document.upgradeElements();
  return definition.ctor;
}

function ancestry(inExtends) {
  var extendee = registry[inExtends];
  if (extendee) {
    return ancestry(extendee.extends).concat([extendee]);
  }
  return [];
}

function resolveTagName(inDefinition) {
  // if we are explicitly extending something, that thing is our
  // baseTag, unless it represents a custom component
  var baseTag = inDefinition.extends;
  // if our ancestry includes custom components, we only have a
  // baseTag if one of them does
  for (var i=0, a; (a=inDefinition.ancestry[i]); i++) {
    baseTag = a.is && a.tag;
  }
  // our tag is our baseTag, if it exists, and otherwise just our name
  inDefinition.tag = baseTag || inDefinition.name;
  if (baseTag) {
    // if there is a base tag, use secondary 'is' specifier
    inDefinition.is = inDefinition.name;
  }
}

function resolvePrototypeChain(inDefinition) {
  // TODO(sjmiles): contains ShadowDOM polyfill pollution
  // if we don't support __proto__ we need to locate the native level
  // prototype for precise mixing in
  // we also need native prototype so we can replace it with a wrapper
  // prototype if necessary
  if (window.WrapperElement
        && !(inDefinition.prototype instanceof WrapperElement)
        || !Object.__proto__) {
    if (inDefinition.is) {
      // for non-trivial extensions, work out both prototypes
      var inst = domCreateElement(inDefinition.tag);
      var wrapperNative = Object.getPrototypeOf(inst);
      if (window.WrapperElement) {
        inst = unwrap(inst);
      }
      var native = Object.getPrototypeOf(inst);
    } else {
      // otherwise, use the defaults
      native = HTMLElement.prototype;
      if (window.WrapperElement) {
        wrapperNative = WrapperHTMLUnknownElement.prototype;
      }
    }
  }
  // cache this in case of mixin
  inDefinition.native = native;
  // under ShadowDOM polyfill we need to use Wrapper prototype chain instead
  // of native DOM
  if (window.WrapperElement
        && !(inDefinition.prototype instanceof WrapperElement)) {
    if (Object.__proto__) {
      // search and replace 'native' protoype with 'wrapperNative'
      var p = inDefinition.prototype, pp;
      while (true) {
        pp = Object.getPrototypeOf(p);
        if (pp === native || pp == HTMLUnknownElement.prototype) {
          break;
        }
        p = pp;
      }
      p.__proto__ = wrapperNative;
      //console.log(native, wrapperNative, p);
    } else {
      // TODO(sjmiles): support IE
    }
  }
}

// SECTION 4

function instantiate(inDefinition) {
  // 4.a.1. Create a new object that implements PROTOTYPE
  // 4.a.2. Let ELEMENT by this new object
  //
  // the custom element instantiation algorithm must also ensure that the
  // output is a valid DOM element with the proper wrapper in place.
  //
  return upgrade(domCreateElement(inDefinition.tag), inDefinition);
}

function upgrade(inElement, inDefinition) {
  // make 'element' implement inDefinition.prototype
  implement(inElement, inDefinition);
  // some definitions specify an 'is' attribute
  if (inDefinition.is) {
    inElement.setAttribute('is', inDefinition.is);
  }
  // flag as upgraded
  inElement.__upgraded__ = true;
  // invoke lifecycle.created callbacks
  created(inElement, inDefinition);
  // OUTPUT
  return inElement;
}

function implement(inElement, inDefinition) {
  // prototype swizzling is best
  if (Object.__proto__) {
    inElement.__proto__ = inDefinition.prototype;
  }
  // TODO(sjmiles): below here is feature detected, but really it's
  // just for IE
  else {
    // where above we can re-acquire inPrototype via
    // getPrototypeOf(Element), we cannot do so when
    // we use mixin, so we install a magic reference
    if (!Object.__proto__) {
      inElement.__proto__ = inDefinition.prototype;
    }
    customMixin(inElement, inDefinition.prototype, inDefinition.native);
  }
}

if (!console.group) {
  console.group = function(m) {console.log("[group] " + m);};
  console.groupEnd = function() {console.log("[end]");};
}

function customMixin(inTarget, inSrc, inNative) {
  //console.group(inTarget.localName);
  // TODO(sjmiles): 'used' allows us to only copy the 'youngest' version of
  // any property. This set should be precalculated. We also need to
  // consider this for supporting 'super'.
  var used = {};
  // start with inSrc
  var p = inSrc;
  // sometimes the default is HTMLUnknownElement.prototype instead of
  // HTMLElement.prototype, so we add a test
  // the idea is to avoid mixing in native prototypes, so adding
  // the second test is WLOG
  while (p !== inNative && p !== HTMLUnknownElement.prototype) {
    //console.group('proto');
    var keys = Object.getOwnPropertyNames(p);
    for (var i=0, k; k=keys[i]; i++) {
      if (!used[k]) {
        //console.log(k);
        Object.defineProperty(inTarget, k,
            Object.getOwnPropertyDescriptor(p, k));
        used[k] = 1;
      }
    }
    //console.groupEnd();
    p = Object.getPrototypeOf(p);
  }
  //console.groupEnd();
}

function created(inElement, inDefinition) {
  var readyCallback = inDefinition.lifecycle.readyCallback ||
      inElement.readyCallback;
  if (readyCallback) {
    readyCallback.call(inElement);
  }
}

var registry = {};
var registrySlctr = '';

function registerDefinition(inName, inDefinition) {
  registry[inName] = inDefinition;
  registrySlctr += (registrySlctr ? ',' : '');
  if (inDefinition.extends) {
    registrySlctr += inDefinition.tag + '[is=' + inDefinition.is + '],';
  }
  registrySlctr += inName;
}

function generateConstructor(inDefinition) {
  return function() {
    return instantiate(inDefinition);
  };
}

function createElement(inTag) {
  var definition = registry[inTag];
  if (definition) {
    return new definition.ctor();
  }
  return domCreateElement(inTag);
}

/**
 * Upgrade an element to a custom element. Upgrading an element
 * causes the custom prototype to be applied, an `is` attribute to be attached
 * (as needed), and invocation of the `readyCallback`.
 * `upgradeElement` does nothing is the element is already upgraded, or
 * if it matches no registered custom tag name.
 *
 * @method ugpradeElement
 * @param {Element} inElement The element to upgrade.
 * @return {Element} The upgraded element.
 */
function upgradeElement(inElement) {
  if (!inElement.__upgraded__) {
    var type = inElement.getAttribute('is') || inElement.localName;
    var definition = registry[type];
    return definition && upgrade(inElement, definition);
  }
}

/**
 * Upgrade all elements under `inRoot` that match selector `inSlctr`.
 * causes the custom prototype to be applied, an `is` attribute to be attached
 * (as needed), and invocation of the `readyCallback`.
 * `upgradeElement` does nothing is the element is already upgraded, or
 * if it matches no registered custom tag name.
 *
 * @method ugpradeElements
 * @param {Node} inRoot The root of the DOM subtree in which elements are to
 *  be upgraded.
 * @param {String} [inSlctr] An optional selector for matching specific
 * elements, otherwise all register element types are upgraded.
 */
function upgradeElements(inRoot, inSlctr) {
  var slctr = inSlctr || registrySlctr;
  if (slctr) {
    var root = inRoot || document;
    forEach(root.querySelectorAll(slctr), upgradeElement);
  }
}

// utilities

var forEach = Array.prototype.forEach.call.bind(Array.prototype.forEach);

// copy all properties from inProps (et al) to inObj
function mixin(inObj/*, inProps, inMoreProps, ...*/) {
  var obj = inObj || {};
  for (var i = 1; i < arguments.length; i++) {
    var p = arguments[i];
    // TODO(sjmiles): on IE we are using mixin
    // to generate custom element instances, as we have
    // no way to alter element prototypes after creation
    // (nor a way to create an element with a custom prototype)
    // however, prototype sources (inSource) are ultimately
    // chained to a native prototype (HTMLElement or inheritor)
    // and trying to copy HTMLElement properties to itself throwss
    // in IE
    // we don't actually want to copy those properties anyway, but I
    // can't find a way to determine if a prototype is a native
    // or custom inheritor of HTMLElement
    // ad hoc solution is to simply stop at the first exception
    // an alternative exists if we have a tagName hint: then we can
    // work out where the native objects are in the prototype chain
    try {
      for (var n in p) {
        copyProperty(n, p, obj);
      }
    } catch(x) {
      //console.log(x);
    }
  }
  return obj;
}

// copy property inName from inSource object to inTarget object
function copyProperty(inName, inSource, inTarget) {
  var pd = getPropertyDescriptor(inSource, inName);
  Object.defineProperty(inTarget, inName, pd);
}

// get property descriptor for inName on inObject, even if
// inName exists on some link in inObject's prototype chain
function getPropertyDescriptor(inObject, inName) {
  if (inObject) {
    var pd = Object.getOwnPropertyDescriptor(inObject, inName);
    return pd || getPropertyDescriptor(Object.getPrototypeOf(inObject), inName);
  }
}

// capture native createElement before we override it
var domCreateElement = document.createElement.bind(document);

// exports

document.register = register;
document.upgradeElement = upgradeElement;
document.upgradeElements = upgradeElements;
document.createElement = createElement; // override

// TODO(sjmiles): temporary, control scope better
window.mixin = mixin;

})();
<|endoftext|>


<commit_before>var Global = require("./global.js");

describe('InstructorScheduleCards', function(){
    describe("Shows the instructed course cards", function() {
        before(function (done) {
            var render_id = 'instructor_course_cards';

            Global.Environment.init({
                render_id: render_id,
                scripts: [
                    "myuw/static/js/card/instructor_schedule/load_course_cards.js",
                    "myuw/static/js/card/instructor_schedule/course_card_content.js",
                    "myuw/static/js/card/instructor_schedule/course_sche_panel.js",
                    "myuw/static/js/card/instructor_schedule/course_resource_panel.js",
                    "myuw/static/js/card/schedule/instructor_panel.js"
                ],
                templates: [
                    'myuw/templates/handlebars/card/instructor_schedule/course_cards.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_card_content.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_resource_panel.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_section.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_sche_panel.html',
                    'myuw/templates/handlebars/card/schedule/instructor_panel.html'
                ]
            });

            Global.Environment.ajax_stub('api/v1/instructor_schedule/2013-spring');

            window.enabled_features = { 'instructor_schedule': true };
            window.location.search = '?grading_date=2017-03-28 16:17';
            $(window).on("myuw:card_load", function () {
                done();
            });

            InstructorCourseCards.term = '2013,spring'
            InstructorCourseCards.dom_target = $('#' + render_id);
            InstructorCourseCards.render_init();
        });
        it("Should render instructor card", function() {
            assert.equal($('.instructed-terms option:selected').val(), '2013,spring');
            assert.equal($('.card').length, 6);
            assert.equal($('.card .courseIDtitle').eq(1).html(), 'PHYS 121 A');
        });
        it("before grading open date", function() {
            window.location.search = '?grading_date=2017-03-02%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        it("immediately before grading open date", function() {
            window.location.search = '?grading_date=2017-03-26%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, true);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        it("while grading open", function() {
            window.location.search = '?grading_date=2017-03-27%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[0].grading_period_is_open, true);
            assert.equal(data.sections[0].grading_status.all_grades_submitted, true);
            assert.equal(data.sections[1].grading_status.all_grades_submitted, false);
        });
        it("immediately before grading deadline", function() {
            window.location.search = '?grading_date=2017-03-28%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, true);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, true);
        });
        it("after grading deadline", function() {
            window.location.search = '?grading_date=2017-04-02%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        after(function () {
            Global.Environment.ajax_stub_restore();
        });
    });
});
<commit_msg>Dropping an out of date test<commit_after>var Global = require("./global.js");

describe('InstructorScheduleCards', function(){
    describe("Shows the instructed course cards", function() {
        before(function (done) {
            var render_id = 'instructor_course_cards';

            Global.Environment.init({
                render_id: render_id,
                scripts: [
                    "myuw/static/js/card/instructor_schedule/load_course_cards.js",
                    "myuw/static/js/card/instructor_schedule/course_card_content.js",
                    "myuw/static/js/card/instructor_schedule/course_sche_panel.js",
                    "myuw/static/js/card/instructor_schedule/course_resource_panel.js",
                    "myuw/static/js/card/schedule/instructor_panel.js"
                ],
                templates: [
                    'myuw/templates/handlebars/card/instructor_schedule/course_cards.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_card_content.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_resource_panel.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_section.html',
                    'myuw/templates/handlebars/card/instructor_schedule/course_sche_panel.html',
                    'myuw/templates/handlebars/card/schedule/instructor_panel.html'
                ]
            });

            Global.Environment.ajax_stub('api/v1/instructor_schedule/2013-spring');

            window.enabled_features = { 'instructor_schedule': true };
            window.location.search = '?grading_date=2017-03-28 16:17';
            $(window).on("myuw:card_load", function () {
                done();
            });

            InstructorCourseCards.term = '2013,spring'
            InstructorCourseCards.dom_target = $('#' + render_id);
            InstructorCourseCards.render_init();
        });
        it("Should render instructor card", function() {
            assert.equal($('.card').length, 6);
            assert.equal($('.card .courseIDtitle').eq(1).html(), 'PHYS 121 A');
        });
        it("before grading open date", function() {
            window.location.search = '?grading_date=2017-03-02%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        it("immediately before grading open date", function() {
            window.location.search = '?grading_date=2017-03-26%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, true);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        it("while grading open", function() {
            window.location.search = '?grading_date=2017-03-27%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[0].grading_period_is_open, true);
            assert.equal(data.sections[0].grading_status.all_grades_submitted, true);
            assert.equal(data.sections[1].grading_status.all_grades_submitted, false);
        });
        it("immediately before grading deadline", function() {
            window.location.search = '?grading_date=2017-03-28%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, true);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, true);
        });
        it("after grading deadline", function() {
            window.location.search = '?grading_date=2017-04-02%2016:17';
            var data = WSData.normalized_instructed_course_data('2013,spring');
            assert.equal(data.sections[1].grading_period_is_open, false);
            assert.equal(data.sections[1].opens_in_24_hours, false);
            assert.equal(data.sections[1].deadline_in_24_hours, false);
        });
        after(function () {
            Global.Environment.ajax_stub_restore();
        });
    });
});
<|endoftext|>


<commit_before>/**
 * Common configuration for the app in both dev an prod mode
 */

import { merge, isFunction } from 'lodash';
import {
  freezeApp,
  pluginLoaded,
  unfreezeApp,
  updatePlugin,
} from 'containers/App/actions';
import auth from 'utils/auth';
import { history, store } from './createStore';
import { translationMessages, languages } from './i18n';
import './public-path';

const isPluginAllowedToRegister = (plugin) => plugin.id === 'users-permissions' || plugin.id === 'email' || auth.getToken();
/**
 * Register a plugin
 *
 * @param params
 */
const registerPlugin = (plugin) => {
  // Merge admin translation messages
  merge(translationMessages, plugin.translationMessages);

  plugin.leftMenuSections = plugin.leftMenuSections || [];
  const shouldAllowRegister = isPluginAllowedToRegister(plugin) !== null;

  switch (true) {
    // Execute bootstrap function and check if plugin can be rendered
    case isFunction(plugin.bootstrap) && isFunction(plugin.pluginRequirements) && shouldAllowRegister:
      plugin.pluginRequirements(plugin)
        .then(plugin => {
          return plugin.bootstrap(plugin);
        })
        .then(plugin => {
          store.dispatch(pluginLoaded(plugin));
        });
      break;
    // Check if plugin can be rendered
    case isFunction(plugin.pluginRequirements):
      plugin.pluginRequirements(plugin).then(plugin => {
        store.dispatch(pluginLoaded(plugin));
      });
      break;
    // Execute bootstrap function
    case isFunction(plugin.bootstrap) && shouldAllowRegister:
      plugin.bootstrap(plugin).then(plugin => {
        store.dispatch(pluginLoaded(plugin));
      });
      break;
    default:
      store.dispatch(pluginLoaded(plugin));
  }
};
const displayNotification = (message, status) => {
  store.dispatch(showNotification(message, status));
};
const lockApp = () => {
  store.dispatch(freezeApp());
};
const unlockApp = () => {
  store.dispatch(unfreezeApp());
};

window.strapi = Object.assign(window.strapi || {}, {
  node: process.env.MODE || 'host',
  registerPlugin,
  notification: {
    success: (message) => {
      displayNotification(message, 'success');
    },
    warning: (message) => {
      displayNotification(message, 'warning');
    },
    error: (message) => {
      displayNotification(message, 'error');
    },
    info: (message) => {
      displayNotification(message, 'info');
    },
  },
  refresh: (pluginId) => ({
    translationMessages: (translationMessagesUpdated) => {
      render(merge({}, translationMessages, translationMessagesUpdated));
    },
    leftMenuSections: (leftMenuSectionsUpdated) => {
      store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
    },
  }),
  router: history,
  languages,
  currentLanguage: window.localStorage.getItem('strapi-admin-language') ||  window.navigator.language ||  window.navigator.userLanguage || 'en',
  lockApp,
  unlockApp,
});<commit_msg>Fix import showNotification<commit_after>/**
 * Common configuration for the app in both dev an prod mode
 */

import { merge, isFunction } from 'lodash';
import {
  freezeApp,
  pluginLoaded,
  unfreezeApp,
  updatePlugin,
} from 'containers/App/actions';
import { showNotification } from 'containers/NotificationProvider/actions';
import auth from 'utils/auth';
import { history, store } from './createStore';
import { translationMessages, languages } from './i18n';
import './public-path';

const isPluginAllowedToRegister = (plugin) => plugin.id === 'users-permissions' || plugin.id === 'email' || auth.getToken();
/**
 * Register a plugin
 *
 * @param params
 */
const registerPlugin = (plugin) => {
  // Merge admin translation messages
  merge(translationMessages, plugin.translationMessages);

  plugin.leftMenuSections = plugin.leftMenuSections || [];
  const shouldAllowRegister = isPluginAllowedToRegister(plugin) !== null;

  switch (true) {
    // Execute bootstrap function and check if plugin can be rendered
    case isFunction(plugin.bootstrap) && isFunction(plugin.pluginRequirements) && shouldAllowRegister:
      plugin.pluginRequirements(plugin)
        .then(plugin => {
          return plugin.bootstrap(plugin);
        })
        .then(plugin => {
          store.dispatch(pluginLoaded(plugin));
        });
      break;
    // Check if plugin can be rendered
    case isFunction(plugin.pluginRequirements):
      plugin.pluginRequirements(plugin).then(plugin => {
        store.dispatch(pluginLoaded(plugin));
      });
      break;
    // Execute bootstrap function
    case isFunction(plugin.bootstrap) && shouldAllowRegister:
      plugin.bootstrap(plugin).then(plugin => {
        store.dispatch(pluginLoaded(plugin));
      });
      break;
    default:
      store.dispatch(pluginLoaded(plugin));
  }
};
const displayNotification = (message, status) => {
  store.dispatch(showNotification(message, status));
};
const lockApp = () => {
  store.dispatch(freezeApp());
};
const unlockApp = () => {
  store.dispatch(unfreezeApp());
};

window.strapi = Object.assign(window.strapi || {}, {
  node: process.env.MODE || 'host',
  registerPlugin,
  notification: {
    success: (message) => {
      displayNotification(message, 'success');
    },
    warning: (message) => {
      displayNotification(message, 'warning');
    },
    error: (message) => {
      displayNotification(message, 'error');
    },
    info: (message) => {
      displayNotification(message, 'info');
    },
  },
  refresh: (pluginId) => ({
    translationMessages: (translationMessagesUpdated) => {
      render(merge({}, translationMessages, translationMessagesUpdated));
    },
    leftMenuSections: (leftMenuSectionsUpdated) => {
      store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));
    },
  }),
  router: history,
  languages,
  currentLanguage: window.localStorage.getItem('strapi-admin-language') ||  window.navigator.language ||  window.navigator.userLanguage || 'en',
  lockApp,
  unlockApp,
});<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');
const Table = require('easy-table');
const parseError = require('@terascope/error-parser');

function makeTable(req, defaults, data, mappingFn) {
    const query = fieldsQuery(req.query, defaults);
    let emptyChar = 'N/A';

    // used to create an empty table if there are no jobs
    if (data.length === 0) {
        emptyChar = '';
        data.push({});
    }

    return Table.print(data, (item, cell) => {
        const fn = mappingFn ? mappingFn(item) : field => _.get(item, field, emptyChar);
        _.each(query, (field) => {
            cell(field, fn(field));
        });
    }, (table) => {
        if (('headers' in req.query) && req.query.headers === 'false') {
            return table.print();
        }
        return table.toString();
    });
}

function fieldsQuery(query, defaults) {
    if (!query.fields) {
        return defaults || [];
    }

    const results = query.fields.split(',').map(word => word.trim());

    if (results.length === 0) {
        return defaults;
    }

    return results;
}

function handleRequest(req, res, defaultErrorMsg = 'Failure to process request', { errorCode = 500, successCode = 200 } = {}) {
    logRequest(req);
    return async (fn) => {
        try {
            const result = await fn();
            if (_.isString(result)) {
                res.status(successCode).send(result);
            } else {
                res.status(successCode).json(result);
            }
        } catch (err) {
            if (_.isError(err) || err.code || err.statusCode) {
                const code = err.statusCode || err.code || errorCode;
                req.logger.error(err.message);
                sendError(res, code, err.message);
                return;
            }

            const errMsg = `${defaultErrorMsg}, error: ${parseError(err)}`;
            req.logger.error(errMsg);
            sendError(res, errorCode, errMsg);
        }
    };
}

function sendError(res, code, error) {
    res.status(code).json({
        error: code,
        message: error
    });
}

// NOTE: This only works for counters, if you're trying to extend this, you
// should probably switch to using prom-client.
function makePrometheus(stats, defaultLabels = {}) {
    const metricMapping = {
        processed: 'teraslice_slices_processed',
        failed: 'teraslice_slices_failed',
        queued: 'teraslice_slices_queued',
        job_duration: '', // this isn't really useful, omitting
        workers_joined: 'teraslice_workers_joined',
        workers_disconnected: 'teraslice_workers_disconnected',
        workers_reconnected: 'teraslice_workers_reconnected'
    };

    let returnString = '';
    _.forEach(stats.controllers, (value, key) => {
        const name = metricMapping[key];
        if (name !== '') {
            returnString += `# TYPE ${name} counter\n`;
            const labels = makePrometheusLabels(defaultLabels);
            returnString += `${name}${labels} ${value}\n`;
        }
    });
    return returnString;
}

function makePrometheusLabels(defaults, custom) {
    const labels = Object.assign({}, defaults, custom);
    const keys = Object.keys(labels);
    if (!keys.length) return '';

    const labelsStr = keys.map((key) => {
        const val = labels[key];
        return `${key}="${val}"`;
    }).join(',');

    return `{${labelsStr}}`;
}

function isPrometheusRequest(req) {
    const acceptHeader = _.get(req, 'headers.accept', '');
    return acceptHeader && acceptHeader.indexOf('application/openmetrics-text;') > -1;
}

function getSearchOptions(req, defaultSort = '_updated:asc') {
    const { size = 100, from = null, sort = defaultSort } = req.query;
    return { size, from, sort };
}

function logRequest(req) {
    const queryInfo = _.map(req.query, (val, key) => `${key}: ${val}`).join(', ');
    const { method, path } = req;
    req.logger.trace(`${_.toUpper(method)} ${path} endpoint has been called, ${queryInfo}`);
}

module.exports = {
    isPrometheusRequest,
    makePrometheus,
    makeTable,
    logRequest,
    getSearchOptions,
    handleRequest,
    sendError
};
<commit_msg>default sort order should _updated:desc not _updated:asc<commit_after>'use strict';

const _ = require('lodash');
const Table = require('easy-table');
const parseError = require('@terascope/error-parser');

function makeTable(req, defaults, data, mappingFn) {
    const query = fieldsQuery(req.query, defaults);
    let emptyChar = 'N/A';

    // used to create an empty table if there are no jobs
    if (data.length === 0) {
        emptyChar = '';
        data.push({});
    }

    return Table.print(data, (item, cell) => {
        const fn = mappingFn ? mappingFn(item) : field => _.get(item, field, emptyChar);
        _.each(query, (field) => {
            cell(field, fn(field));
        });
    }, (table) => {
        if (('headers' in req.query) && req.query.headers === 'false') {
            return table.print();
        }
        return table.toString();
    });
}

function fieldsQuery(query, defaults) {
    if (!query.fields) {
        return defaults || [];
    }

    const results = query.fields.split(',').map(word => word.trim());

    if (results.length === 0) {
        return defaults;
    }

    return results;
}

function handleRequest(req, res, defaultErrorMsg = 'Failure to process request', { errorCode = 500, successCode = 200 } = {}) {
    logRequest(req);
    return async (fn) => {
        try {
            const result = await fn();
            if (_.isString(result)) {
                res.status(successCode).send(result);
            } else {
                res.status(successCode).json(result);
            }
        } catch (err) {
            if (_.isError(err) || err.code || err.statusCode) {
                const code = err.statusCode || err.code || errorCode;
                req.logger.error(err.message);
                sendError(res, code, err.message);
                return;
            }

            const errMsg = `${defaultErrorMsg}, error: ${parseError(err)}`;
            req.logger.error(errMsg);
            sendError(res, errorCode, errMsg);
        }
    };
}

function sendError(res, code, error) {
    res.status(code).json({
        error: code,
        message: error
    });
}

// NOTE: This only works for counters, if you're trying to extend this, you
// should probably switch to using prom-client.
function makePrometheus(stats, defaultLabels = {}) {
    const metricMapping = {
        processed: 'teraslice_slices_processed',
        failed: 'teraslice_slices_failed',
        queued: 'teraslice_slices_queued',
        job_duration: '', // this isn't really useful, omitting
        workers_joined: 'teraslice_workers_joined',
        workers_disconnected: 'teraslice_workers_disconnected',
        workers_reconnected: 'teraslice_workers_reconnected'
    };

    let returnString = '';
    _.forEach(stats.controllers, (value, key) => {
        const name = metricMapping[key];
        if (name !== '') {
            returnString += `# TYPE ${name} counter\n`;
            const labels = makePrometheusLabels(defaultLabels);
            returnString += `${name}${labels} ${value}\n`;
        }
    });
    return returnString;
}

function makePrometheusLabels(defaults, custom) {
    const labels = Object.assign({}, defaults, custom);
    const keys = Object.keys(labels);
    if (!keys.length) return '';

    const labelsStr = keys.map((key) => {
        const val = labels[key];
        return `${key}="${val}"`;
    }).join(',');

    return `{${labelsStr}}`;
}

function isPrometheusRequest(req) {
    const acceptHeader = _.get(req, 'headers.accept', '');
    return acceptHeader && acceptHeader.indexOf('application/openmetrics-text;') > -1;
}

function getSearchOptions(req, defaultSort = '_updated:desc') {
    const { size = 100, from = null, sort = defaultSort } = req.query;
    return { size, from, sort };
}

function logRequest(req) {
    const queryInfo = _.map(req.query, (val, key) => `${key}: ${val}`).join(', ');
    const { method, path } = req;
    req.logger.trace(`${_.toUpper(method)} ${path} endpoint has been called, ${queryInfo}`);
}

module.exports = {
    isPrometheusRequest,
    makePrometheus,
    makeTable,
    logRequest,
    getSearchOptions,
    handleRequest,
    sendError
};
<|endoftext|>


<commit_before>import {context} from './canvas/context';
import {isArray} from 'vega-util';
import {lru} from './lru';

// width computation is expensive so we are memoizing it
const TEXT_WIDTH_CACHE_SIZE = 10000;
const widthCache = lru(TEXT_WIDTH_CACHE_SIZE);

var currFontHeight, currentFont;

export var textMetrics = {
  height: fontSize,
  measureWidth: measureWidth,
  estimateWidth: estimateWidth,
  width: estimateWidth,
  canvas: useCanvas
};

useCanvas(true);

// make dumb, simple estimate if no canvas is available
function estimateWidth(item, text) {
  currFontHeight = fontSize(item);
  return estimate(textValue(item, text));
}

function estimate(text) {
  return ~~(0.8 * text.length * currFontHeight);
}

// measure text width if canvas is available
function measureWidth(item, text) {
  return fontSize(item) <= 0 ? 0
    : (currentFont = font(item), measure(textValue(item, text)));
}

function measure(text) {
  const key = currentFont + text;
  let width = widthCache.get(key);
  if (width === undefined) {
    context.font = currentFont;
    width = context.measureText(text).width;
    widthCache.set(key, width);
  }
  return width;
}

export function fontSize(item) {
  return item.fontSize != null ? item.fontSize : 11;
}

function useCanvas(use) {
  textMetrics.width = (use && context) ? measureWidth : estimateWidth;
}

export function lineHeight(item) {
  return item.lineHeight != null ? item.lineHeight : (fontSize(item) + 2);
}

function lineArray(_) {
  return isArray(_) ? _.length > 1 ? _ : _[0] : _;
}

export function textLines(item) {
  return lineArray(
    item.lineBreak && item.text && !isArray(item.text)
      ? item.text.split(item.lineBreak)
      : item.text
  );
}

export function multiLineOffset(item) {
  const tl = textLines(item);
  return (isArray(tl) ? (tl.length - 1) : 0) * lineHeight(item);
}

export function textValue(item, line) {
  return line == null ? ''
    : item.limit > 0 ? truncate(item, line)
    : line + '';
}

function truncate(item, line) {
  var limit = +item.limit,
      text = line + '',
      width;

  if (textMetrics.width === measureWidth) {
    // we are using canvas
    currentFont = font(item);
    width = measure;
  } else {
    // we are relying on estimates
    currFontHeight = fontSize(item);
    width = estimate;
  }

  if (width(text) < limit) return text;

  var ellipsis = item.ellipsis || '\u2026',
      rtl = item.dir === 'rtl',
      lo = 0,
      hi = text.length, mid;

  limit -= width(ellipsis);

  if (rtl) {
    while (lo < hi) {
      mid = (lo + hi >>> 1);
      if (width(text.slice(mid)) > limit) lo = mid + 1;
      else hi = mid;
    }
    return ellipsis + text.slice(lo);
  } else {
    while (lo < hi) {
      mid = 1 + (lo + hi >>> 1);
      if (width(text.slice(0, mid)) < limit) lo = mid;
      else hi = mid - 1;
    }
    return text.slice(0, lo) + ellipsis;
  }
}

export function fontFamily(item, quote) {
  var font = item.font;
  return (quote && font
    ? String(font).replace(/"/g, '\'')
    : font) || 'sans-serif';
}

export function font(item, quote) {
  return '' +
    (item.fontStyle ? item.fontStyle + ' ' : '') +
    (item.fontVariant ? item.fontVariant + ' ' : '') +
    (item.fontWeight ? item.fontWeight + ' ' : '') +
    fontSize(item) + 'px ' +
    fontFamily(item, quote);
}

export function offset(item) {
  // perform our own font baseline calculation
  // why? not all browsers support SVG 1.1 'alignment-baseline' :(
  var baseline = item.baseline,
      h = fontSize(item);
  return Math.round(
    baseline === 'top'    ?  0.79*h :
    baseline === 'middle' ?  0.30*h :
    baseline === 'bottom' ? -0.21*h : 0
  );
}
<commit_msg>Spearate elements of hash key<commit_after>import {context} from './canvas/context';
import {isArray} from 'vega-util';
import {lru} from './lru';

// width computation is expensive so we are memoizing it
const TEXT_WIDTH_CACHE_SIZE = 10000;
const widthCache = lru(TEXT_WIDTH_CACHE_SIZE);

var currFontHeight, currentFont;

export var textMetrics = {
  height: fontSize,
  measureWidth: measureWidth,
  estimateWidth: estimateWidth,
  width: estimateWidth,
  canvas: useCanvas
};

useCanvas(true);

// make dumb, simple estimate if no canvas is available
function estimateWidth(item, text) {
  currFontHeight = fontSize(item);
  return estimate(textValue(item, text));
}

function estimate(text) {
  return ~~(0.8 * text.length * currFontHeight);
}

// measure text width if canvas is available
function measureWidth(item, text) {
  return fontSize(item) <= 0 ? 0
    : (currentFont = font(item), measure(textValue(item, text)));
}

function measure(text) {
  const key = `${currentFont}_${text}`;
  let width = widthCache.get(key);
  if (width === undefined) {
    context.font = currentFont;
    width = context.measureText(text).width;
    widthCache.set(key, width);
  }
  return width;
}

export function fontSize(item) {
  return item.fontSize != null ? item.fontSize : 11;
}

function useCanvas(use) {
  textMetrics.width = (use && context) ? measureWidth : estimateWidth;
}

export function lineHeight(item) {
  return item.lineHeight != null ? item.lineHeight : (fontSize(item) + 2);
}

function lineArray(_) {
  return isArray(_) ? _.length > 1 ? _ : _[0] : _;
}

export function textLines(item) {
  return lineArray(
    item.lineBreak && item.text && !isArray(item.text)
      ? item.text.split(item.lineBreak)
      : item.text
  );
}

export function multiLineOffset(item) {
  const tl = textLines(item);
  return (isArray(tl) ? (tl.length - 1) : 0) * lineHeight(item);
}

export function textValue(item, line) {
  return line == null ? ''
    : item.limit > 0 ? truncate(item, line)
    : line + '';
}

function truncate(item, line) {
  var limit = +item.limit,
      text = line + '',
      width;

  if (textMetrics.width === measureWidth) {
    // we are using canvas
    currentFont = font(item);
    width = measure;
  } else {
    // we are relying on estimates
    currFontHeight = fontSize(item);
    width = estimate;
  }

  if (width(text) < limit) return text;

  var ellipsis = item.ellipsis || '\u2026',
      rtl = item.dir === 'rtl',
      lo = 0,
      hi = text.length, mid;

  limit -= width(ellipsis);

  if (rtl) {
    while (lo < hi) {
      mid = (lo + hi >>> 1);
      if (width(text.slice(mid)) > limit) lo = mid + 1;
      else hi = mid;
    }
    return ellipsis + text.slice(lo);
  } else {
    while (lo < hi) {
      mid = 1 + (lo + hi >>> 1);
      if (width(text.slice(0, mid)) < limit) lo = mid;
      else hi = mid - 1;
    }
    return text.slice(0, lo) + ellipsis;
  }
}

export function fontFamily(item, quote) {
  var font = item.font;
  return (quote && font
    ? String(font).replace(/"/g, '\'')
    : font) || 'sans-serif';
}

export function font(item, quote) {
  return '' +
    (item.fontStyle ? item.fontStyle + ' ' : '') +
    (item.fontVariant ? item.fontVariant + ' ' : '') +
    (item.fontWeight ? item.fontWeight + ' ' : '') +
    fontSize(item) + 'px ' +
    fontFamily(item, quote);
}

export function offset(item) {
  // perform our own font baseline calculation
  // why? not all browsers support SVG 1.1 'alignment-baseline' :(
  var baseline = item.baseline,
      h = fontSize(item);
  return Math.round(
    baseline === 'top'    ?  0.79*h :
    baseline === 'middle' ?  0.30*h :
    baseline === 'bottom' ? -0.21*h : 0
  );
}
<|endoftext|>


<commit_before>import Clock from './clock.vue'

export default {
  id         : 'clock',
  name       : 'Clock',
  description: `Display a configurable clock`,
  components : [
    {
      name         : 'Clock',
      tag          : 'clock',
      vue          : Clock,
      defaultWidth : 8,
      defaultHeight: 4
    }
  ]
}
<commit_msg>fix(plugin-clock): upgrade default size<commit_after>import Clock from './clock.vue'

export default {
  id         : 'clock',
  name       : 'Clock',
  description: `Display a configurable clock`,
  components : [
    {
      name         : 'Clock',
      tag          : 'clock',
      vue          : Clock,
      defaultWidth : 8,
      defaultHeight: 3
    }
  ]
}
<|endoftext|>