<commit_before>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function() {
    'use strict';

    /**
     * Creates a new KeyNavigator tied to the provided ScrollList
     *
     * @classdesc
     *
     * A key navigator watches for keypresses and scrolls the ScrollList
     * in response to Up/Left/Down/Right arrow, Home/End, Ctrl+Home/Ctrl+End,
     * and PageUp/PageDown. The responses to these actions are intended to
     * approximate standard key navigation behavior in the browser.
     *
     * @name KeyNavigator
     * @constructor
     *
     * @param {ScrollList} scrollList
     *        The scrollList that the KeyNavigator is associated with.
     *
     */

    var KeyNavigator = function (scrollList) {
        this._scrollList = scrollList;

        // Watch for keydown events and if the documentViewer is enabled,
        // translate the event into the appropriate scrolling

        document.addEventListener('keydown', (this._keyNavListener).bind(this));
    };

    /* Keycodes */
    var keys = {
        LEFT: 37,
        RIGHT: 39,
        UP: 38,
        DOWN: 40,
        PAGEUP: 33,
        PAGEDOWN: 34,
        HOME: 36,
        END: 35
    };

    KeyNavigator.prototype = {
        /**
         * Determine whether an event needs to be handled
         *
         * @private
         * @param {Event} event
         *        The event in question
         */
        _keyNavListener: function(event) {
            if (!this._scrollList.isDisabled()) {
                this._layout = this._scrollList.getLayout();
                this._handleKeyPress(event);
            }
        },

        /**
         * Identify and handle key press events by passing the event to the appropriate handler
         *
         * @private
         * @param {Event} event
         *        The event to be handled
         */

        _handleKeyPress: function (event) {
            if ( event.keyCode === keys.LEFT || event.keyCode === keys.RIGHT ) {
                this._moveX(event.keyCode);
            }

            if ( event.keyCode === keys.UP || event.keyCode === keys.DOWN ) {
                this._moveY(event.keyCode);
            }

            if ( event.keyCode === keys.PAGEUP || event.keyCode === keys.PAGEDOWN ) {
                this._movePage(event.keyCode);
            }

            if ( event.keyCode === keys.HOME || event.keyCode === keys.END ) {
                if ( event.ctrlKey ) {
                    this._moveCtrlHomeEnd(event.keyCode);
                }
                else {
                    this._moveHomeEnd(event.keyCode);
                }
            }
        },

        /**
         * Move the scrollList horizontally by 40.
         *
         * @private
         * @param {number} direction
         *        Which horizontal direction to move in (left or right)
         */
        _moveX: function (direction) {
            var currentX = -this._scrollList.getListMap().getCurrentTransformState().translateX;

            if (direction === keys.LEFT) {
                this._scrollList.scrollToPosition({x: currentX - 40});
            }
            else {

                this._scrollList.scrollToPosition({x: currentX + 40});
            }
        },

        /**
         * Move the scrollList vertically by 40
         *
         * @private
         * @param {number} direction
         *        Which vertical direction to move in (up or down)
         */
        _moveY: function (direction) {
            var currentY = this._layout.getVisiblePosition().top;
            if (direction === keys.UP) {
                this._scrollList.scrollToPosition({y: currentY - 40});
            }
            else {
                if (this._layout.getVisiblePosition().bottom === this._layout.getSize().height) {
                    return;
                }
                else {
                    this._scrollList.scrollToPosition({y: currentY + 40});
                }
            }
        },

        /**
         * Move up or down by one page
         *
         * @private
         * @param {number} direction
         *        The direction to move by a page (PageUp or PageDown)
         */
        _movePage: function (direction) {
            var currentPage = this._scrollList.getCurrentItem();
            var currentPosition = this._layout.getVisiblePosition();

            var itemHeight = this._scrollList.getItemSizeCollection();
            itemHeight = itemHeight._items[currentPage.index].height;

            if (direction === keys.PAGEUP) {
                this._scrollList.scrollToPosition({y: currentPosition.top - itemHeight});
            }
            else {
                if (currentPosition.bottom === this._layout.getSize().height) {
                    return;
                }
                this._scrollList.scrollToPosition({y: currentPosition.top + itemHeight});
            }
        },

        /**
         * Move to the top or the bottom of the current page
         *
         * @private
         * @param {number} direction
         *        The keyCode indicating whether to go to home or end
         */
        _moveHomeEnd: function (direction) {
            var currentPage = this._scrollList.getCurrentItem();
            var items = this._scrollList.getItemSizeCollection()._items;
            var position = 0;

            if (direction === keys.HOME) {
                for (var i = 0; i < currentPage.index; i++ ) {
                    position += items[i].height;
                }

                position = position * 1.5;
                position += ( currentPage.index * 5 );

            }

            if (direction === keys.END) {
                for (var j = 0; j <= currentPage.index; j++ ) {
                    position += items[j].height;
                }

                position = position * 1.5;
                position += ( currentPage.index * 5 );
                position -= (this._layout.getVisiblePosition().bottom - this._layout.getVisiblePosition().top);
            }

            this._scrollList.scrollToPosition({y: position});
        },

        /**
         * Move to the beginning or end of the scrollList
         *
         * @private
         * @param {number} direction
         *        The direction to move in (Home or End)
         */
        _moveCtrlHomeEnd: function (direction) {
            if ( direction === keys.HOME ) {
                this._scrollList.scrollToPosition({y: 0});
            }
            else {
                var height = this._layout.getSize().height;
                this._scrollList.scrollToPosition({y: height});
            }
        }
    };

    return KeyNavigator;
});
<commit_msg>Fix inconsistent spacing in KeyNavigator<commit_after>/*
 * Copyright 2014 WebFilings, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

define(function() {
    'use strict';

    /**
     * Creates a new KeyNavigator tied to the provided ScrollList
     *
     * @classdesc
     *
     * A key navigator watches for keypresses and scrolls the ScrollList
     * in response to Up/Left/Down/Right arrow, Home/End, Ctrl+Home/Ctrl+End,
     * and PageUp/PageDown. The responses to these actions are intended to
     * approximate standard key navigation behavior in the browser.
     *
     * @name KeyNavigator
     * @constructor
     *
     * @param {ScrollList} scrollList
     *        The scrollList that the KeyNavigator is associated with.
     *
     */

    var KeyNavigator = function (scrollList) {
        this._scrollList = scrollList;

        // Watch for keydown events and if the documentViewer is enabled,
        // translate the event into the appropriate scrolling

        document.addEventListener('keydown', (this._keyNavListener).bind(this));
    };

    /* Keycodes */
    var keys = {
        LEFT: 37,
        RIGHT: 39,
        UP: 38,
        DOWN: 40,
        PAGEUP: 33,
        PAGEDOWN: 34,
        HOME: 36,
        END: 35
    };

    KeyNavigator.prototype = {
        /**
         * Determine whether an event needs to be handled
         *
         * @private
         * @param {Event} event
         *        The event in question
         */
        _keyNavListener: function(event) {
            if (!this._scrollList.isDisabled()) {
                this._layout = this._scrollList.getLayout();
                this._handleKeyPress(event);
            }
        },

        /**
         * Identify and handle key press events by passing the event to the appropriate handler
         *
         * @private
         * @param {Event} event
         *        The event to be handled
         */

        _handleKeyPress: function(event) {
            if (event.keyCode === keys.LEFT || event.keyCode === keys.RIGHT) {
                this._moveX(event.keyCode);
            }

            if (event.keyCode === keys.UP || event.keyCode === keys.DOWN) {
                this._moveY(event.keyCode);
            }

            if (event.keyCode === keys.PAGEUP || event.keyCode === keys.PAGEDOWN) {
                this._movePage(event.keyCode);
            }

            if (event.keyCode === keys.HOME || event.keyCode === keys.END) {
                if (event.ctrlKey) {
                    this._moveCtrlHomeEnd(event.keyCode);
                }
                else {
                    this._moveHomeEnd(event.keyCode);
                }
            }
        },

        /**
         * Move the scrollList horizontally by 40.
         *
         * @private
         * @param {number} direction
         *        Which horizontal direction to move in (left or right)
         */
        _moveX: function(direction) {
            var currentX = -this._scrollList.getListMap().getCurrentTransformState().translateX;
            if (direction === keys.LEFT) {
                this._scrollList.scrollToPosition({ x: currentX - 40 });
            }
            else {
                this._scrollList.scrollToPosition({ x: currentX + 40 });
            }
        },

        /**
         * Move the scrollList vertically by 40
         *
         * @private
         * @param {number} direction
         *        Which vertical direction to move in (up or down)
         */
        _moveY: function(direction) {
            var currentY = this._layout.getVisiblePosition().top;
            if (direction === keys.UP) {
                this._scrollList.scrollToPosition({ y: currentY - 40 });
            }
            else {
                if (this._layout.getVisiblePosition().bottom === this._layout.getSize().height) {
                    return;
                }
                else {
                    this._scrollList.scrollToPosition({ y: currentY + 40 });
                }
            }
        },

        /**
         * Move up or down by one page
         *
         * @private
         * @param {number} direction
         *        The direction to move by a page (PageUp or PageDown)
         */
        _movePage: function(direction) {
            var currentPage = this._scrollList.getCurrentItem();
            var currentPosition = this._layout.getVisiblePosition();

            var itemHeight = this._scrollList.getItemSizeCollection();
            itemHeight = itemHeight._items[currentPage.index].height;

            if (direction === keys.PAGEUP) {
                this._scrollList.scrollToPosition({ y: currentPosition.top - itemHeight });
            }
            else {
                if (currentPosition.bottom === this._layout.getSize().height) {
                    return;
                }
                this._scrollList.scrollToPosition({ y: currentPosition.top + itemHeight });
            }
        },

        /**
         * Move to the top or the bottom of the current page
         *
         * @private
         * @param {number} direction
         *        The keyCode indicating whether to go to home or end
         */
        _moveHomeEnd: function(direction) {
            var currentPage = this._scrollList.getCurrentItem();
            var items = this._scrollList.getItemSizeCollection()._items;
            var position = 0;

            if (direction === keys.HOME) {
                for (var i = 0; i < currentPage.index; i++) {
                    position += items[i].height;
                }

                position = position * 1.5;
                position += (currentPage.index * 5);

            }

            if (direction === keys.END) {
                for (var j = 0; j <= currentPage.index; j++) {
                    position += items[j].height;
                }

                position = position * 1.5;
                position += (currentPage.index * 5);
                position -= (this._layout.getVisiblePosition().bottom - this._layout.getVisiblePosition().top);
            }

            this._scrollList.scrollToPosition({ y: position });
        },

        /**
         * Move to the beginning or end of the scrollList
         *
         * @private
         * @param {number} direction
         *        The direction to move in (Home or End)
         */
        _moveCtrlHomeEnd: function(direction) {
            if (direction === keys.HOME) {
                this._scrollList.scrollToPosition({ y: 0 });
            }
            else {
                var height = this._layout.getSize().height;
                this._scrollList.scrollToPosition({ y: height });
            }
        }
    };

    return KeyNavigator;
});
<|endoftext|>


<commit_before>//////////////////////////////
// Nunjucks
//  - A Gulp Plugin
//
// Builds Nunjucks to HTML
//////////////////////////////
'use strict';

//////////////////////////////
// Variables
//////////////////////////////
var through = require('through2'),
    gutil = require('gulp-util'),
    path = require('path'),
    fs = require('fs-extra'),
    dateformat = require('dateformat'),
    fm = require('front-matter'),
    marked = require('./markdown'),
    PluginError = gutil.PluginError,
    PLUGIN_NAME = 'nunjucks',
    gulpNunjucks;

//////////////////////////////
// Set Nunjucks stuff
//////////////////////////////

//////////////////////////////
// Export
//////////////////////////////
gulpNunjucks = function (options) {
  options = options || {};

  var nunjucks = gulpNunjucks.compiler;
  var nunjucksPaths = options.paths;
  var nunjucksEnv;

  nunjucksEnv = nunjucks.configure(nunjucksPaths, {
    'noCache': true
  });

  //////////////////////////////
  // Nunjucks Filters and Tags
  //////////////////////////////
  nunjucksEnv.addFilter('attributes', function (file, attribute) {
    var content;
    file = fs.readFileSync(path.join(process.cwd(), file));

    content = fm(file.toString());

    if (content.attributes) {
      if (attribute) {
        return content.attributes[attribute];
      }
      else {
        return content.attributes;
      }
    }
    else {
      return {};
    }

    return file;
  });

  nunjucksEnv.addFilter('markdown', function (content) {
    return marked(content);
  });

  nunjucksEnv.addFilter('date', function (content, format) {
    return dateformat(content, format);
  });

  // Iterate over all custom defined filters and add them
  Object.keys(options.filters).forEach(function (filter) {
    nunjucksEnv.addFilter(filter, options.filters[filter]);
  });

  // Iterate over all custom defined tags and add them
  Object.keys(options.tags).forEach(function (tag) {
    nunjucksEnv.addExtension(tag, options.tags[tag]);
  });

  //////////////////////////////
  // Command line arguments for each option
  //////////////////////////////
  process.argv.forEach(function (val, index, array) {
    if (index >= 3) {
      switch (val) {
        case '--fail': options.failOnError = true; break;
      }
    }
  });

  //////////////////////////////
  // Through Object
  //////////////////////////////
  var compile = through.obj(function (file, encoding, cb) {
    var ext = path.extname(file.path),
        context = {};

    /////////////////////////////
    // Default plugin issues
    //////////////////////////////
    if (file.isNull()) {
      return cb();
    }
    if (file.isStream()) {
      this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!'));
      return cb();
    }

    //////////////////////////////
    // Manipulate Files
    //////////////////////////////
    if (ext === '.html') {
      if (file.meta) {
        context = file.meta;
        context.filename = file.path;

        if (!file.meta.published) {
          context.published = file.stat.birthtime;
        }
        if (!file.meta.updated) {
          context.updated = file.stat.mtime;
        }

        context.stats = file.stat;

        // If a template exists in the meta info, build a content block and extend for it
        if (file.meta.template) {
          file.contents = new Buffer(nunjucksEnv.renderString(
            '{% extends "' + file.meta.template + '" %}' +
            '{% block content %}' + file.contents.toString() + '{% endblock %}'
            , context)
          );
        }
        // Otherwise, just render it!
        else {
          file.contents = new Buffer(nunjucksEnv.renderString(file.contents.toString(), context));
        }
      }
    }

    //////////////////////////////
    // Push the file back to the stream!
    //////////////////////////////
    this.push(file);

    //////////////////////////////
    // Callback to tell us we're done
    //////////////////////////////
    cb();
  })

  return compile;
}

gulpNunjucks.compiler = require('nunjucks');

gulpNunjucks.filters = {};
gulpNunjucks.tags = {};
gulpNunjucks.paths = [];

module.exports = gulpNunjucks;
<commit_msg>:new: Add Render filter<commit_after>//////////////////////////////
// Nunjucks
//  - A Gulp Plugin
//
// Builds Nunjucks to HTML
//////////////////////////////
'use strict';

//////////////////////////////
// Variables
//////////////////////////////
var through = require('through2'),
    gutil = require('gulp-util'),
    path = require('path'),
    fs = require('fs-extra'),
    dateformat = require('dateformat'),
    fm = require('front-matter'),
    marked = require('./markdown'),
    PluginError = gutil.PluginError,
    PLUGIN_NAME = 'nunjucks',
    gulpNunjucks;

//////////////////////////////
// Set Nunjucks stuff
//////////////////////////////

//////////////////////////////
// Export
//////////////////////////////
gulpNunjucks = function (options) {
  options = options || {};

  var nunjucks = gulpNunjucks.compiler;
  var nunjucksPaths = options.paths;
  var nunjucksEnv;

  nunjucksEnv = nunjucks.configure(nunjucksPaths, {
    'noCache': true
  });

  //////////////////////////////
  // Nunjucks Filters and Tags
  //////////////////////////////
  nunjucksEnv.addFilter('attributes', function (file, attribute) {
    var content;
    file = fs.readFileSync(path.join(process.cwd(), file));

    content = fm(file.toString());

    if (content.attributes) {
      if (attribute) {
        return content.attributes[attribute];
      }
      else {
        return content.attributes;
      }
    }
    else {
      return {};
    }

    return file;
  });

  nunjucksEnv.addFilter('render', function (file) {
    var content;
    file = fs.readFileSync(path.join(process.cwd(), file));

    content = fm(file.toString()).body;

    return marked(content);
  });

  nunjucksEnv.addFilter('markdown', function (content) {
    return marked(content);
  });

  nunjucksEnv.addFilter('date', function (content, format) {
    return dateformat(content, format);
  });

  // Iterate over all custom defined filters and add them
  Object.keys(options.filters).forEach(function (filter) {
    nunjucksEnv.addFilter(filter, options.filters[filter]);
  });

  // Iterate over all custom defined tags and add them
  Object.keys(options.tags).forEach(function (tag) {
    nunjucksEnv.addExtension(tag, options.tags[tag]);
  });

  //////////////////////////////
  // Command line arguments for each option
  //////////////////////////////
  process.argv.forEach(function (val, index, array) {
    if (index >= 3) {
      switch (val) {
        case '--fail': options.failOnError = true; break;
      }
    }
  });

  //////////////////////////////
  // Through Object
  //////////////////////////////
  var compile = through.obj(function (file, encoding, cb) {
    var ext = path.extname(file.path),
        context = {};

    /////////////////////////////
    // Default plugin issues
    //////////////////////////////
    if (file.isNull()) {
      return cb();
    }
    if (file.isStream()) {
      this.emit('error', new PluginError(PLUGIN_NAME, 'Streams are not supported!'));
      return cb();
    }

    //////////////////////////////
    // Manipulate Files
    //////////////////////////////
    if (ext === '.html') {
      if (file.meta) {
        context = file.meta;
        context.filename = file.path;

        if (!file.meta.published) {
          context.published = file.stat.birthtime;
        }
        if (!file.meta.updated) {
          context.updated = file.stat.mtime;
        }

        context.stats = file.stat;

        // If a template exists in the meta info, build a content block and extend for it
        if (file.meta.template) {
          file.contents = new Buffer(nunjucksEnv.renderString(
            '{% extends "' + file.meta.template + '" %}' +
            '{% block content %}' + file.contents.toString() + '{% endblock %}'
            , context)
          );
        }
        // Otherwise, just render it!
        else {
          file.contents = new Buffer(nunjucksEnv.renderString(file.contents.toString(), context));
        }
      }
    }

    //////////////////////////////
    // Push the file back to the stream!
    //////////////////////////////
    this.push(file);

    //////////////////////////////
    // Callback to tell us we're done
    //////////////////////////////
    cb();
  })

  return compile;
}

gulpNunjucks.compiler = require('nunjucks');

gulpNunjucks.filters = {};
gulpNunjucks.tags = {};
gulpNunjucks.paths = [];

module.exports = gulpNunjucks;
<|endoftext|>


<commit_before>const service = require('feathers-mongoose');
const { newsModel, newsHistoryModel } = require('./model');
const hooks = require('./hooks');

const flatten = arr => arr.reduce((agg, el) => {
	if (el instanceof Array) {
		return agg.concat(flatten(el));
	}
	return agg.concat(el);
}, []);

const paginate = (data, params) => {
	if (params.paginate === false) {
		return data;
	}
	const limit = params.$limit || data.length;
	const skip = params.$skip || 0;
	const paginatedData = data.slice(skip, skip + limit);
	return {
		total: data.length,
		limit,
		skip,
		data: paginatedData,
	};
};

class NewsService {
	setup(app) {
		this.app = app;
	}

	async findSchoolNews({ userId, schoolId }) {
		const user = await this.app.service('users').get(userId);
		const hasAccess = user.permissions.includes('NEWS_VIEW');
		if (!hasAccess) {
			throw new Error('Mising permissions to view school news.');
		}
		return newsModel.find({	schoolId });
	}

	async findScopedNews(userId, target) {
		const scopes = await newsModel.distinct('targetModel');
		const ops = scopes.map(async (scope) => {
			const scopeListService = this.app.service(`/users/:scopeId/${scope}`);
			if (scopeListService === undefined) {
				throw new Error(`Missing ScopeListService for scope "${scope}".`);
			}
			let scopeItems = await scopeListService.find({
				route: { scopeId: userId.toString() },
				query: { permissions: ['NEWS_VIEW'] },
			});
			if (target) {
				scopeItems = scopeItems.filter(i => i._id.toString() === target.toString());
			}
			return Promise.all(scopeItems.map(async (item) => {
				const news = await newsModel.find({
					targetModel: scope,
					target: item._id,
				}).lean();
				return Promise.all(news.map(async n => ({
					...n,
					target: await this.app.service(scope).get(n.target),
				})));
			}));
		});
		const results = await Promise.all(ops);
		return flatten(results);
	}

	/**
	 * GET /news/
	 * Returns all news the user can see.
	 * @param {*} params
	 * @returns array of news items
	 * @memberof NewsService
	 */
	async find(params) {
		let news = [];
		const scoped = params.query && params.query.target;
		if (scoped) {
			news = news.concat(await this.findScopedNews(params.account.userId, params.query.target));
		} else {
			news = news.concat(await this.findSchoolNews(params.account));
			news = news.concat(await this.findScopedNews(params.account.userId));
		}
		return Promise.resolve(paginate(news, params));
	}
}

module.exports = function news() {
	const app = this;
	const newsService = new NewsService();
	app.use('/news', newsService);
	app.service('news').hooks(hooks);

	app.use('/newshistory', service({
		Model: newsHistoryModel,
	}));
	const newsHistoryService = app.service('/newshistory');
	newsHistoryService.hooks(hooks);
};
<commit_msg>Document methods<commit_after>const service = require('feathers-mongoose');
const { newsModel, newsHistoryModel } = require('./model');
const hooks = require('./hooks');

/**
 * Recursively flattens an array
 * @param {Array} arr array to flatten
 * @example flatten([1, [2], [[3, 4], 5], 6]) => [1, 2, 3, 4, 5, 6]
 * @returns {Array} flatted array
 */
const flatten = arr => arr.reduce((agg, el) => {
	if (el instanceof Array) {
		return agg.concat(flatten(el));
	}
	return agg.concat(el);
}, []);

/**
 * Emulates Feathers-style pagination on a given array.
 * @param {Array} data Array-like collection to paginate
 * @param {Object} params Feathers request params containing paginate, $limit, and $skip
 * @returns {Object} { total, limit, skip, data }
 */
const paginate = (data, params) => {
	if (params.paginate === false) {
		return data;
	}
	const limit = params.$limit || data.length;
	const skip = params.$skip || 0;
	const paginatedData = data.slice(skip, skip + limit);
	return {
		total: data.length,
		limit,
		skip,
		data: paginatedData,
	};
};

class NewsService {
	setup(app) {
		this.app = app;
	}

	/**
	 * Returns all school news the user is allowed to see.
	 * @param {Object} { userId, schoolId } -- The user's Id and schoolId
	 * @returns Array<News Document>
	 * @memberof NewsService
	 */
	async findSchoolNews({ userId, schoolId }) {
		const user = await this.app.service('users').get(userId);
		const hasAccess = user.permissions.includes('NEWS_VIEW');
		if (!hasAccess) {
			throw new Error('Mising permissions to view school news.');
		}
		return newsModel.find({	schoolId }).lean();
	}

	/**
	 * Returns scoped news the user is allowed to see
	 *
	 * @param {BsonId|String} userId the user's Id
	 * @param {BsonId|String} target (optional) Id of the news target (course, team, etc.)
	 * @returns Array<News Document>
	 * @memberof NewsService
	 */
	async findScopedNews(userId, target) {
		const scopes = await newsModel.distinct('targetModel');
		const ops = scopes.map(async (scope) => {
			// For each possible target model, find all targets the user has NEWS_VIEW permissions in.
			const scopeListService = this.app.service(`/users/:scopeId/${scope}`);
			if (scopeListService === undefined) {
				throw new Error(`Missing ScopeListService for scope "${scope}".`);
			}
			let scopeItems = await scopeListService.find({
				route: { scopeId: userId.toString() },
				query: { permissions: ['NEWS_VIEW'] },
			});
			if (target) {
				// if a target id is given, only return news from this target
				scopeItems = scopeItems.filter(i => i._id.toString() === target.toString());
			}
			return Promise.all(scopeItems.map(async (item) => {
				const news = await newsModel.find({
					targetModel: scope,
					target: item._id,
				}).lean();
				// Manually populate the target (current API requires this):
				return Promise.all(news.map(async n => ({
					...n,
					target: await this.app.service(scope).get(n.target),
				})));
			}));
		});
		const results = await Promise.all(ops);
		return flatten(results);
	}

	/**
	 * GET /news/
	 * Returns all news the user can see.
	 * @param {*} params
	 * @returns array of news items
	 * @memberof NewsService
	 */
	async find(params) {
		let news = [];
		const scoped = params.query && params.query.target;
		if (scoped) {
			news = news.concat(await this.findScopedNews(params.account.userId, params.query.target));
		} else {
			news = news.concat(await this.findSchoolNews(params.account));
			news = news.concat(await this.findScopedNews(params.account.userId));
		}
		return Promise.resolve(paginate(news, params));
	}
}

module.exports = function news() {
	const app = this;
	const newsService = new NewsService();
	app.use('/news', newsService);
	app.service('news').hooks(hooks);

	app.use('/newshistory', service({
		Model: newsHistoryModel,
	}));
	const newsHistoryService = app.service('/newshistory');
	newsHistoryService.hooks(hooks);
};
<|endoftext|>


<commit_before>//
//   Copyright 2009-2014 Ilkka Oksanen <iao@iki.fi>
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing,
//   software distributed under the License is distributed on an "AS
//   IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
//   express or implied.  See the License for the specific language
//   governing permissions and limitations under the License.
//

'use strict';

/* globals $ */

import Ember from 'ember';

export default Ember.Controller.extend({
    store: Ember.inject.service(),

    init() {
        this._super();

        // We are service and fully initialized only after this run loop.
        Ember.run.next(this, function() {
            this.get('store').start();
        });
    }
});
<commit_msg>Simplify index controller<commit_after>//
//   Copyright 2009-2014 Ilkka Oksanen <iao@iki.fi>
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing,
//   software distributed under the License is distributed on an "AS
//   IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
//   express or implied.  See the License for the specific language
//   governing permissions and limitations under the License.
//

'use strict';

import Ember from 'ember';

export default Ember.Controller.extend({
    store: Ember.inject.service(),

    init() {
        this._super();
        this.get('store').start(); // Everything starts here
    }
});
<|endoftext|>


<commit_before>import {setFocus} from '../components/fef-flying-focus';
import {FefStorage} from '../classes/fef-storage';
import {KEYCODES} from '../utils/fef-keycodes';

export function init() {
    $('.js-search').each((i, elem) => {
        new SrfSearch(elem);
    });
}

const DEFAULT_MAX_SUGGESTIONS = 7;
const DEFAULT_MIN_SEARCH_LENGTH = 2;

const ACTIVE_CLASS = 'search--active';
const OUTSIDE_CLICK_LISTENER_NAME = 'click.search-destroyer';
const LOCAL_STORAGE_KEY = 'srf:search:history';

export class SrfSearch {

    constructor(element, options) {
        this.$element = $(element);
        this.$inputField = this.$element.find('.js-search-input');
        this.$searchResults = this.$element.find('.js-search-results');
        this.$closeIcon = this.$element.find('.js-search-close');

        this.options = $.extend({}, {
            maxSuggestionCount: DEFAULT_MAX_SUGGESTIONS,
            minSearchLength: DEFAULT_MIN_SEARCH_LENGTH
        }, options);

        this.typeaheadUrl = this.$element.attr('data-typeahead-url');
        this.clickedSuggestionsEnabled = this.$element.data('suggestion-history');
        this.typeaheadData = null;
        this.suggestionUrl = '';
        this.currTimeout = null;

        // search field is hidden before document.ready (events firing before document.ready can get lost)
        this.$inputField.addClass('search__input--loaded');
        this.registerListeners();
    }

    registerListeners() {
        // e.g. in the search modal, the whole element will be focused -> set focus to inputfield
        this.$element.on('focus', (e) => {
            setTimeout(() => {
                this.$inputField.focus();
            }, 0);
        });

        this.$inputField.on('focus', (e) => {
            this.setSearchActive();

            if (this.clickedSuggestionsEnabled
                && FefStorage.isLocalStorageAvailable()
                && FefStorage.hasItem(LOCAL_STORAGE_KEY)
                && this.$inputField.val() === '') {
                const storedResults = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                this.renderResults(storedResults, '');
            }
        }).on('keyup', (e) => {
            this.onKeyUp(e);
        }).on('keydown', (e) => {
            this.onKeyDown(e);
        }).on('click', (e) => {
            e.stopPropagation();
        });

        this.$closeIcon.on('click', (e) => {
            e.preventDefault();
            this.setSearchInactive();
            this.clearInput();
        }).on('keydown', (e) => {
            this.onCloseIconKeyDown(e);
        });

        this.$searchResults.on('keydown', (e) => {
            this.onResultsKeyDown(e);
        });
    }

    onKeyUp(e) {
        switch (e.keyCode) {
            case KEYCODES.tab:
                break;
            case KEYCODES.escape:
                this.$inputField.blur();
                this.setSearchInactive();
                this.clearInput();
                break;
            default:
                let query = this.$inputField.val().toString().toLowerCase();
                this.lookup(query);
        }
    }

    onKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.tab:
                if (e.shiftKey) {
                    // Shift-Tabbing out of the search component
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            case KEYCODES.up:
                this.moveUpInMenu();
                e.preventDefault();
                break;
            case KEYCODES.down:
                this.moveDownInMenu();
                e.preventDefault();
                break;
            default:
                break;
        }
    }

    onCloseIconKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.escape:
                this.setSearchInactive();
                this.clearInput();
                break;
            case KEYCODES.tab:
                if (this.$searchResults.children().length === 0) {
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            default:
                break;
        }
    }

    onResultsKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.escape:
                this.setSearchInactive();
                this.clearInputIfAllowed();
                break;
            case KEYCODES.tab:
                // tabbing away from the last result
                if ($(e.target).parents('li').is(':last-child') && !e.shiftKey) {
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            case KEYCODES.up:
                this.moveUpInMenu();
                e.preventDefault();
                break;
            case KEYCODES.down:
                this.moveDownInMenu();
                e.preventDefault();
                break;
            default:
                break;
        }
    }

    setSearchActive() {
        if (this.$element.hasClass(ACTIVE_CLASS)) {
            return false;
        }

        this.$element.addClass(ACTIVE_CLASS);
        // Listen to clicks outside of the element --> deactivates this search component
        $(document).on(OUTSIDE_CLICK_LISTENER_NAME, (e) => {
            // Disable this search if the click was not a descendant of any .js-search or if it's a descendant of a different search component
            if (!$(e.target).parents('.js-search').length && $(e.target).parents('.js-search') !== this.$element) {
                this.setSearchInactive();
            }
        });

        if (this.typeaheadData === null) {
            $.getJSON(this.typeaheadUrl, (data) => {
                this.typeaheadData = data;

                if (this.clickedSuggestionsEnabled
                    && FefStorage.isLocalStorageAvailable
                    && FefStorage.hasItem(LOCAL_STORAGE_KEY)) {
                    let items = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                    items = items.filter((storedItem) => {
                        return this.typeaheadData.find((fetchedItem) => { return (fetchedItem.name === storedItem.name && fetchedItem.url === storedItem.url); });
                    });

                    FefStorage.setItemJsonStringified(LOCAL_STORAGE_KEY, items);
                }
            });
        }

        this.$closeIcon.show();
    }

    setSearchInactive() {
        this.hideResults();
        this.$element.removeClass(ACTIVE_CLASS);
        $(document).off(OUTSIDE_CLICK_LISTENER_NAME);
        this.$element.find('.search-result__alert').empty();
    }

    moveUpInMenu() {
        let $suggestions = this.$searchResults.find('.search-result__link'),
            $currentlyFocused = $('.search-result__link:focus'),
            currentIndex = $suggestions.index($currentlyFocused),
            nextIndex = currentIndex - 1;

        if ($suggestions.length === 0) {
            return;
        }

        if (nextIndex < 0) {
            nextIndex = $suggestions.length - 1;
        }

        setFocus($suggestions.eq(nextIndex));
    }

    moveDownInMenu() {
        let $suggestions = this.$searchResults.find('.search-result__link'),
            $currentlyFocused = $('.search-result__link:focus'),
            currentIndex = $suggestions.index($currentlyFocused),
            nextIndex = currentIndex + 1;

        if ($suggestions.length === 0) {
            return;
        }

        if (nextIndex >= $suggestions.length) {
            nextIndex = 0;
        }

        setFocus($suggestions.eq(nextIndex));
    }

    hideResults() {
        this.$inputField.attr('aria-expanded', false);
        this.suggestionUrl = '';
        this.$element.find('.search-result__alert').empty();

        /**
         *  setTimeout fixes a different behavior in ie 11, which causes not being able to leave the input field.
         */
        setTimeout(() => {
            this.$searchResults.hide().html('').removeClass ('search__results--showed-results');
        }, 0);

    }

    clearInput() {
        this.$inputField.val('');
        this.$closeIcon.hide();
        this.suggestionUrl = '';
    }

    clearInputIfAllowed() {
        // clear input if data-keep-keyword-on-blur was not set
        if (this.$inputField.data('keep-keyword-on-blur') === undefined) {
            this.clearInput();
        }
    }

    lookup(query) {
        let results = [];

        if (this.typeaheadData === null) {
            return true;
        }

        if (query.length < this.options.minSearchLength) {
            this.hideResults();
            return true;
        }

        this.typeaheadData.forEach((item) => {
            let matchIndex = item.name.toString().toLowerCase().indexOf(query);
            if (matchIndex >= 0) {
                results.push({
                    name: item.name,
                    url: item.url,
                    matchIndex: matchIndex
                });
            }
        });

        if (results.length > 0) {
            results = results.slice(0, this.options.maxSuggestionCount);
            this.renderResults(results, query);
        } else {
            this.hideResults();
        }
    }

    /**
     * Renders the received search results into the results list.
     * For the Screen-Reader's sake it'll be rendered twice, once with the found substring highlighted and once readable.
     *
     * If we're replacing previously shown search results, make sure they're not animated anymore.
     *
     * @param {Object} results
     * @param {String} query
     */
    renderResults(results, query) {
        let html = '';
        const wasAlreadyShowingResults = this.$searchResults.children('li').length > 0;
        const resultLength = results.length;

        this.$searchResults.html('');

        results.map((result, index) => {
            let highlightedResult = this.highlightQuery(query, result.name);
            const $li = $('<li>', { class: `typeahead-suggestion ${wasAlreadyShowingResults ? 'typeahead-suggestion--no-animation' : ''}`});
            const $link = $('<a>', { class: 'search-result__link', href: result.url, 'data-referrer-track' : 'srg_override_mod1=search_suggestions&srg_override_mod2=search_result&srg_override_mod3=' + query + '&srg_override_mod4=' + resultLength + '-' + (index + 1)});

            $link.on('click', () => {

                this.$inputField.val('');

                if (this.clickedSuggestionsEnabled && FefStorage.isLocalStorageAvailable()) {
                    let storedResults = [];
                    if (FefStorage.hasItem(LOCAL_STORAGE_KEY)) {
                        storedResults = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                    }

                    storedResults = storedResults.filter((item) => {
                        return (item.name !== result.name && item.url !== result.url);
                    });

                    storedResults.unshift(result);
                    storedResults = storedResults.slice(0, this.options.maxSuggestionCount);
                    FefStorage.setItemJsonStringified(LOCAL_STORAGE_KEY, storedResults);
                }

            });

            $('<span>', { role: 'presentation', 'aria-hidden': true }).append(highlightedResult).appendTo($link);
            $('<span>', { class: 'h-offscreen' }).text(result.name).appendTo($link);

            $li.append($link);
            $li.appendTo(this.$searchResults);
        });

        this.$searchResults.show();

        // additional screenreader info (same behavior like meteo search field)
        let $result_alert = $('.search-result__alert');
        let result_html = `${results.length} ${this.$inputField.data('result-alert-text')} <span>${query}</span>`;

        if ($result_alert.length === 0) {
            this.$searchResults.before(`<span class="h-offscreen search-result__alert" role="alert">${result_html}</span>`);
        } else {
            $result_alert.html(result_html);
        }

        this.$inputField.attr('aria-expanded', true);
    }

    /**
     * Replaces the query string in a search result with the same string in bold  e.g.
     * "Tag" in "Tagesschau" after searching for "Tag"
     *
     * @param {String} query
     * @param {String} name
     * @returns {String}
     */
    highlightQuery(query, name) {
        query = query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
        return name.replace(new RegExp('(' + query + ')', 'ig'), ($1, match) => `<strong>${match}</strong>`);
    }
}
<commit_msg>Keymatch-Search in FEF-Typeahead<commit_after>import {setFocus} from '../components/fef-flying-focus';
import {FefStorage} from '../classes/fef-storage';
import {KEYCODES} from '../utils/fef-keycodes';

export function init() {
    $('.js-search').each((i, elem) => {
        new SrfSearch(elem);
    });
}

const DEFAULT_MAX_SUGGESTIONS = 7;
const DEFAULT_MIN_SEARCH_LENGTH = 2;

const ACTIVE_CLASS = 'search--active';
const OUTSIDE_CLICK_LISTENER_NAME = 'click.search-destroyer';
const LOCAL_STORAGE_KEY = 'srf:search:history';

export class SrfSearch {

    constructor(element, options) {
        this.$element = $(element);
        this.$inputField = this.$element.find('.js-search-input');
        this.$searchResults = this.$element.find('.js-search-results');
        this.$closeIcon = this.$element.find('.js-search-close');

        this.options = $.extend({}, {
            maxSuggestionCount: DEFAULT_MAX_SUGGESTIONS,
            minSearchLength: DEFAULT_MIN_SEARCH_LENGTH
        }, options);

        this.typeaheadUrl = this.$element.attr('data-typeahead-url');
        this.clickedSuggestionsEnabled = this.$element.data('suggestion-history');
        this.typeaheadData = null;
        this.suggestionUrl = '';
        this.currTimeout = null;

        // search field is hidden before document.ready (events firing before document.ready can get lost)
        this.$inputField.addClass('search__input--loaded');
        this.registerListeners();
    }

    registerListeners() {
        // e.g. in the search modal, the whole element will be focused -> set focus to inputfield
        this.$element.on('focus', (e) => {
            setTimeout(() => {
                this.$inputField.focus();
            }, 0);
        });

        this.$inputField.on('focus', (e) => {
            this.setSearchActive();

            if (this.clickedSuggestionsEnabled
                && FefStorage.isLocalStorageAvailable()
                && FefStorage.hasItem(LOCAL_STORAGE_KEY)
                && this.$inputField.val() === '') {
                const storedResults = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                this.renderResults(storedResults, '');
            }
        }).on('keyup', (e) => {
            this.onKeyUp(e);
        }).on('keydown', (e) => {
            this.onKeyDown(e);
        }).on('click', (e) => {
            e.stopPropagation();
        });

        this.$closeIcon.on('click', (e) => {
            e.preventDefault();
            this.setSearchInactive();
            this.clearInput();
        }).on('keydown', (e) => {
            this.onCloseIconKeyDown(e);
        });

        this.$searchResults.on('keydown', (e) => {
            this.onResultsKeyDown(e);
        });
    }

    onKeyUp(e) {
        switch (e.keyCode) {
            case KEYCODES.tab:
                break;
            case KEYCODES.escape:
                this.$inputField.blur();
                this.setSearchInactive();
                this.clearInput();
                break;
            default:
                let query = this.$inputField.val().toString().toLowerCase();
                this.lookup(query);
        }
    }

    onKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.tab:
                if (e.shiftKey) {
                    // Shift-Tabbing out of the search component
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            case KEYCODES.up:
                this.moveUpInMenu();
                e.preventDefault();
                break;
            case KEYCODES.down:
                this.moveDownInMenu();
                e.preventDefault();
                break;
            default:
                break;
        }
    }

    onCloseIconKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.escape:
                this.setSearchInactive();
                this.clearInput();
                break;
            case KEYCODES.tab:
                if (this.$searchResults.children().length === 0) {
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            default:
                break;
        }
    }

    onResultsKeyDown(e) {
        switch (e.keyCode) {
            case KEYCODES.escape:
                this.setSearchInactive();
                this.clearInputIfAllowed();
                break;
            case KEYCODES.tab:
                // tabbing away from the last result
                if ($(e.target).parents('li').is(':last-child') && !e.shiftKey) {
                    this.setSearchInactive();
                    this.clearInputIfAllowed();
                }
                break;
            case KEYCODES.up:
                this.moveUpInMenu();
                e.preventDefault();
                break;
            case KEYCODES.down:
                this.moveDownInMenu();
                e.preventDefault();
                break;
            default:
                break;
        }
    }

    setSearchActive() {
        if (this.$element.hasClass(ACTIVE_CLASS)) {
            return false;
        }

        this.$element.addClass(ACTIVE_CLASS);
        // Listen to clicks outside of the element --> deactivates this search component
        $(document).on(OUTSIDE_CLICK_LISTENER_NAME, (e) => {
            // Disable this search if the click was not a descendant of any .js-search or if it's a descendant of a different search component
            if (!$(e.target).parents('.js-search').length && $(e.target).parents('.js-search') !== this.$element) {
                this.setSearchInactive();
            }
        });

        if (this.typeaheadData === null) {
            $.getJSON(this.typeaheadUrl, (data) => {
                this.typeaheadData = data;

                if (this.clickedSuggestionsEnabled
                    && FefStorage.isLocalStorageAvailable
                    && FefStorage.hasItem(LOCAL_STORAGE_KEY)) {
                    let items = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                    items = items.filter((storedItem) => {
                        return this.typeaheadData.find((fetchedItem) => { return (fetchedItem.name === storedItem.name && fetchedItem.url === storedItem.url); });
                    });

                    FefStorage.setItemJsonStringified(LOCAL_STORAGE_KEY, items);
                }
            });
        }

        this.$closeIcon.show();
    }

    setSearchInactive() {
        this.hideResults();
        this.$element.removeClass(ACTIVE_CLASS);
        $(document).off(OUTSIDE_CLICK_LISTENER_NAME);
        this.$element.find('.search-result__alert').empty();
    }

    moveUpInMenu() {
        let $suggestions = this.$searchResults.find('.search-result__link'),
            $currentlyFocused = $('.search-result__link:focus'),
            currentIndex = $suggestions.index($currentlyFocused),
            nextIndex = currentIndex - 1;

        if ($suggestions.length === 0) {
            return;
        }

        if (nextIndex < 0) {
            nextIndex = $suggestions.length - 1;
        }

        setFocus($suggestions.eq(nextIndex));
    }

    moveDownInMenu() {
        let $suggestions = this.$searchResults.find('.search-result__link'),
            $currentlyFocused = $('.search-result__link:focus'),
            currentIndex = $suggestions.index($currentlyFocused),
            nextIndex = currentIndex + 1;

        if ($suggestions.length === 0) {
            return;
        }

        if (nextIndex >= $suggestions.length) {
            nextIndex = 0;
        }

        setFocus($suggestions.eq(nextIndex));
    }

    hideResults() {
        this.$inputField.attr('aria-expanded', false);
        this.suggestionUrl = '';
        this.$element.find('.search-result__alert').empty();

        /**
         *  setTimeout fixes a different behavior in ie 11, which causes not being able to leave the input field.
         */
        setTimeout(() => {
            this.$searchResults.hide().html('').removeClass ('search__results--showed-results');
        }, 0);

    }

    clearInput() {
        this.$inputField.val('');
        this.$closeIcon.hide();
        this.suggestionUrl = '';
    }

    clearInputIfAllowed() {
        // clear input if data-keep-keyword-on-blur was not set
        if (this.$inputField.data('keep-keyword-on-blur') === undefined) {
            this.clearInput();
        }
    }

    lookup(query) {
        let results = [];

        if (this.typeaheadData === null) {
            return true;
        }

        if (query.length < this.options.minSearchLength) {
            this.hideResults();
            return true;
        }

        this.typeaheadData.forEach((item) => {
            const matchIndexName = item.name.toString().toLowerCase().indexOf(query);
            const matchIndexKey  = item.keymatches.toString().toLowerCase().indexOf(query);
            let matchIndex = Math.max(matchIndexName, matchIndexKey);
            if (matchIndex >= 0) {
                results.push({
                    name: item.name,
                    url: item.url,
                    matchIndex: matchIndex
                });
            }
        });

        if (results.length > 0) {
            results = results.slice(0, this.options.maxSuggestionCount);
            this.renderResults(results, query);
        } else {
            this.hideResults();
        }
    }

    /**
     * Renders the received search results into the results list.
     * For the Screen-Reader's sake it'll be rendered twice, once with the found substring highlighted and once readable.
     *
     * If we're replacing previously shown search results, make sure they're not animated anymore.
     *
     * @param {Object} results
     * @param {String} query
     */
    renderResults(results, query) {
        let html = '';
        const wasAlreadyShowingResults = this.$searchResults.children('li').length > 0;
        const resultLength = results.length;

        this.$searchResults.html('');

        results.map((result, index) => {
            let highlightedResult = this.highlightQuery(query, result.name);
            const $li = $('<li>', { class: `typeahead-suggestion ${wasAlreadyShowingResults ? 'typeahead-suggestion--no-animation' : ''}`});
            const $link = $('<a>', { class: 'search-result__link', href: result.url, 'data-referrer-track' : 'srg_override_mod1=search_suggestions&srg_override_mod2=search_result&srg_override_mod3=' + query + '&srg_override_mod4=' + resultLength + '-' + (index + 1)});

            $link.on('click', () => {

                this.$inputField.val('');

                if (this.clickedSuggestionsEnabled && FefStorage.isLocalStorageAvailable()) {
                    let storedResults = [];
                    if (FefStorage.hasItem(LOCAL_STORAGE_KEY)) {
                        storedResults = FefStorage.getItemJsonParsed(LOCAL_STORAGE_KEY);
                    }

                    storedResults = storedResults.filter((item) => {
                        return (item.name !== result.name && item.url !== result.url);
                    });

                    storedResults.unshift(result);
                    storedResults = storedResults.slice(0, this.options.maxSuggestionCount);
                    FefStorage.setItemJsonStringified(LOCAL_STORAGE_KEY, storedResults);
                }

            });

            $('<span>', { role: 'presentation', 'aria-hidden': true }).append(highlightedResult).appendTo($link);
            $('<span>', { class: 'h-offscreen' }).text(result.name).appendTo($link);

            $li.append($link);
            $li.appendTo(this.$searchResults);
        });

        this.$searchResults.show();

        // additional screenreader info (same behavior like meteo search field)
        let $result_alert = $('.search-result__alert');
        let result_html = `${results.length} ${this.$inputField.data('result-alert-text')} <span>${query}</span>`;

        if ($result_alert.length === 0) {
            this.$searchResults.before(`<span class="h-offscreen search-result__alert" role="alert">${result_html}</span>`);
        } else {
            $result_alert.html(result_html);
        }

        this.$inputField.attr('aria-expanded', true);
    }

    /**
     * Replaces the query string in a search result with the same string in bold  e.g.
     * "Tag" in "Tagesschau" after searching for "Tag"
     *
     * @param {String} query
     * @param {String} name
     * @returns {String}
     */
    highlightQuery(query, name) {
        query = query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
        return name.replace(new RegExp('(' + query + ')', 'ig'), ($1, match) => `<strong>${match}</strong>`);
    }
}
<|endoftext|>


<commit_before>import R from "ramda";
import React from "react";
import Immutable from "immutable";
import { delay, localStorage } from "js-util";
import rest from "rest-middleware/browser";
import bdd from "./bdd";
import apiConsole from "./api-console";

const LOG_LIST = Symbol("log-list");
const COMPONENT = Symbol("component");

/**
 * The API used internally by the UIHarness components.
 */
class Api {
  constructor() {
    this.current = Immutable.Map();
    this[LOG_LIST] = Immutable.List();
    this.loadInvokeCount = 0;
  }

  /**
   * Initializes the UIHarness environment.
   * @param {Function} callback: Invoked when ready to initialize the DOM.
   */
  init(callback) {
    // Put state into global namespace.
    bdd.register();
    global.UIHarness = global.uih = apiConsole;

    // Insert the <Shell> into the root.
    //    NB: Signal DOM ready after a delay to ensure that the [describe/it]
    //        have fully parsed before initial render. Avoids a redraw.
    delay(() => {
        // Ensure the last loaded suite is set as the current state.
        const suite = this.lastSelectedSuite();
        if (suite) {
          this.loadSuite(this.lastSelectedSuite(), { storeAsLastSuite:false });
        }
        callback()
    });

    // Finish up.
    return this;
  }

  /**
   * Resets the internal API.
   * @param {boolean} hard: Flag indicating if all state from local-storage
   *                        should be cleared away, or just current selection state.
   */
  reset({ hard = true } = {}) {
    if (hard) {
      this.clearLocalStorage();
    } else {
      this.clearLocalStorage("lastInvokedSpec:");
    }
    this.lastSelectedSuite(null);
    this.setCurrent(null);
    this.component(null);
    return this;
  }

  /**
   * Removes all ui-harness values stored in local-storage.
   */
  clearLocalStorage(startsWith = null) {
    localStorage.keys().forEach(key => {
        let match = "ui-harness:";
        if (startsWith) { match += startsWith; }
        if (key.startsWith(match)) {
          localStorage.prop(key, null); // Remove.
        }
      });
  }

  /**
   * Gets or sets the current component instance.
   * Pass {null} to clear.
   */
  component(value) {
    // WRITE.
    if (value !== undefined) {
      if (value === null && this[COMPONENT]) {
        // Unload component.
        delete this[COMPONENT];
        delete apiConsole.component;
        this.setCurrent({
          componentType: null,
          componentProps: null,
          componentChildren: null,
          component: null
        });

      } else {

        // Store component instance.
        this[COMPONENT] = value;
        apiConsole.component = value;
        if (this.current.get("component") !== value) {
          // NB: Preform instance comparison before updating the
          //     current state to prevent render loop.
          this.setCurrent({ component: value });
        }
      }
    }

    // READ.
    return this[COMPONENT];
  }

  /**
   * Loads the current suite into the Harness.
   *
   * @param suite: The {Suite} to load.
   * @param options
   *          - storeAsLastSuite: Flag indicating if the suite should be stored
   *                              as the last invoked suite in localStorage.
   *                              Default: true.
   */
  loadSuite(suite, { storeAsLastSuite = true } = {}) {
    // Setup initial conditions.
    if (!suite) { return this; }
    if (this.current.get("suite") === suite) { return this; }

    // Only load the suite if it does not have children
    // ie. is not a container/folder suite.
    if (suite.childSuites.length === 0) {
      // Clear the current state.
      this.setCurrent(null);

      // Prepare the new current state.
      let current = suite.meta.thisContext.toValues();
      current.suite = suite;
      current.indexMode = this.indexMode();
      current.isBeforeInvoked = false;
      this.setCurrent(current);
      if (storeAsLastSuite) { this.lastSelectedSuite(suite); }

      // Invoke before handlers.
      this.invokeBeforeHandlers(suite);

      // If the last invoked spec on the suite contained a load.
      let lastInvokedSpec = this.lastInvokedSpec(suite);
      if (lastInvokedSpec && lastInvokedSpec.spec && lastInvokedSpec.isLoader) {
        this.invokeSpec(lastInvokedSpec.spec);
      }
    }

    // Finish up.
    return this;
  }

  /**
   * Invokes the [before] handlers for
   * the given suite if required.
   * @return {boolean}  - true if the handlers were invoked
   *                    - false if they have already been invoked.
   */
  invokeBeforeHandlers(suite) {
    if (this.current.get("isBeforeInvoked")) { return false; }
    const self = suite.meta.thisContext;
    suite.beforeHandlers.invoke(self);
    this.current = this.current.set("isBeforeInvoked", true);
    return true
  }

  /**
   * Invokes the given spec.
   * @param spec: The [Spec] to invoke.
   * @param callback: Invoked upon completion.
   *                   Immediately if the spec is not asynchronous.
   */
  invokeSpec(spec, callback) {
    // Setup initial conditions.
    const suite = spec.parentSuite;
    const self = suite.meta.thisContext;
    this.invokeBeforeHandlers(suite)
    let loadInvokeCountBefore = this.loadInvokeCount;

    // Invoke the methods.
    spec.invoke(self, callback);

    // Store a reference to last-invoked spec.
    this.lastInvokedSpec(suite, {
      spec: spec,
      isLoader: (this.loadInvokeCount > loadInvokeCountBefore)
    });

    // Increment the current invoke count for the spec.
    let specInvokeCount = this.current.get("specInvokeCount") || {};
    let total = specInvokeCount[spec.id] || 0;
    specInvokeCount[spec.id] = total + 1;
    this.setCurrent({ specInvokeCount: specInvokeCount });

    // Finish up.
    return this;
  }

  /**
   * Invokes a spec on the server.
   * @param spec: The [Spec] to invoke.
   * @param callback: Invoked upon completion.
   */
  invokeServerSpec(spec, callback) {
    const server = rest();
    server.onReady(() => {
        server.methods.invokeSpec.put(spec.id)
        .then((result) => {
          // TODO:
          console.log("result", result);
        });
    });
  }

  /**
   * Gets or sets the last selected [Suite].
   */
  lastSelectedSuite(suite) {
    if (suite) { suite = suite.id; }
    const result = this.localStorage("lastSelectedSuite", suite);
    return bdd.suites[result];
  }

  /**
   * Gets or sets the last spec for the given suite
   * that was invoked that had a `.load()` call within it.
   */
  lastInvokedSpec(suite, { spec, isLoader = false} = {}) {
    const KEY = `lastInvokedSpec:${ suite.id }`;
    let value;
    if (spec !== undefined) {
      // WRITE.
      value = { spec: spec.id, isLoader: isLoader }
      spec = spec.id;
    }

    // READ.
    let result = this.localStorage(KEY, value);
    if (result) {
      result.spec = R.find(spec => spec.id === result.spec, suite.specs);
    }
    return result
  }

  /**
   * Gets or sets the display mode of the left-hand index.
   * @param {string} mode: tree|suite
   */
  indexMode(mode) {
    let result = this.localStorage("indexMode", mode, { default: "tree" });
    if (mode !== undefined) {
      // WRITE (store in current state).
      this.setCurrent({ indexMode:mode });
    }

    // READ.
    result = result || "tree";
    if (result !== "tree" && this.current.get("suite") === undefined) {
      result = "tree"
    }
    return result;
  }

  /**
   * Updates the current state with the given values.
   *     NOTE: These values are cumulatively added to the state.
   *           Use "reset" to clear the state.
   *
   * @param args:  An object containing they [key:values] to set
   *               or null to clear values.
   */
  setCurrent(args) {
    // Update the state object.
    if (args) {
      Object.keys(args).forEach(key => { this.current = this.current.set(key, args[key]) });
    } else {
      this.current = this.current.clear();
    }

    // Apply to the <Shell>.
    if (this.shell) { this.shell.setState({ current: this.current }); }
    return this;
  };

  /**
   * Logs a value to the output.
   * @param {array} values: The value or values to append.
   */
  log(...values) {
    values = R.flatten(values);
    const item = { time: new Date(), values };
    this[LOG_LIST] = this[LOG_LIST].push(item);
    this.setCurrent({ log: this[LOG_LIST], showLog: true });
    return this;
  }

  /**
   * Clears the output log.
   */
  clearLog() {
    // console.log("clear log");
    this[LOG_LIST] = this[LOG_LIST].clear();
    this.setCurrent({ log: this[LOG_LIST], showLog: false });

  }

  /**
   * Provides common access to localStorage.
   * @param key:         The unique identifier of the value (this is prefixed with the namespace).
   * @param value:       (optional). The value to set (pass null to remove).
   * @param options:
   *           default:  (optional). The default value to return if the session does not contain the value (ie. undefined).
   *
   * @return the read value.
   */
  localStorage(key, value, options) {
    return localStorage.prop(`ui-harness:${ key }`, value, options);
  }
}

// Singleton instance.
export default new Api();
<commit_msg>Remove undefined values from current object.<commit_after>import R from "ramda";
import React from "react";
import Immutable from "immutable";
import { delay, localStorage } from "js-util";
import rest from "rest-middleware/browser";
import bdd from "./bdd";
import apiConsole from "./api-console";

const LOG_LIST = Symbol("log-list");
const COMPONENT = Symbol("component");

/**
 * The API used internally by the UIHarness components.
 */
class Api {
  constructor() {
    this.current = Immutable.Map();
    this[LOG_LIST] = Immutable.List();
    this.loadInvokeCount = 0;
  }

  /**
   * Initializes the UIHarness environment.
   * @param {Function} callback: Invoked when ready to initialize the DOM.
   */
  init(callback) {
    // Put state into global namespace.
    bdd.register();
    global.UIHarness = global.uih = apiConsole;

    // Insert the <Shell> into the root.
    //    NB: Signal DOM ready after a delay to ensure that the [describe/it]
    //        have fully parsed before initial render. Avoids a redraw.
    delay(() => {
        // Ensure the last loaded suite is set as the current state.
        const suite = this.lastSelectedSuite();
        if (suite) {
          this.loadSuite(this.lastSelectedSuite(), { storeAsLastSuite:false });
        }
        callback()
    });

    // Finish up.
    return this;
  }

  /**
   * Resets the internal API.
   * @param {boolean} hard: Flag indicating if all state from local-storage
   *                        should be cleared away, or just current selection state.
   */
  reset({ hard = true } = {}) {
    if (hard) {
      this.clearLocalStorage();
    } else {
      this.clearLocalStorage("lastInvokedSpec:");
    }
    this.lastSelectedSuite(null);
    this.setCurrent(null);
    this.component(null);
    return this;
  }

  /**
   * Removes all ui-harness values stored in local-storage.
   */
  clearLocalStorage(startsWith = null) {
    localStorage.keys().forEach(key => {
        let match = "ui-harness:";
        if (startsWith) { match += startsWith; }
        if (key.startsWith(match)) {
          localStorage.prop(key, null); // Remove.
        }
      });
  }

  /**
   * Gets or sets the current component instance.
   * Pass {null} to clear.
   */
  component(value) {
    // WRITE.
    if (value !== undefined) {
      if (value === null) {
        // Unload component.
        delete this[COMPONENT];
        delete apiConsole.component;
        this.setCurrent({
          componentType: undefined,
          componentProps: undefined,
          componentChildren: undefined,
          component: undefined
        });

      } else {

        // Store component instance.
        this[COMPONENT] = value;
        apiConsole.component = value;
        if (this.current.get("component") !== value) {
          // NB: Preform instance comparison before updating the
          //     current state to prevent render loop.
          this.setCurrent({ component: value });
        }
      }
    }

    // READ.
    return this[COMPONENT];
  }

  /**
   * Loads the current suite into the Harness.
   *
   * @param suite: The {Suite} to load.
   * @param options
   *          - storeAsLastSuite: Flag indicating if the suite should be stored
   *                              as the last invoked suite in localStorage.
   *                              Default: true.
   */
  loadSuite(suite, { storeAsLastSuite = true } = {}) {
    // Setup initial conditions.
    if (!suite) { return this; }
    if (this.current.get("suite") === suite) { return this; }

    // Only load the suite if it does not have children
    // ie. is not a container/folder suite.
    if (suite.childSuites.length === 0) {
      // Clear the current state.
      this.setCurrent(null);

      // Prepare the new current state.
      let current = suite.meta.thisContext.toValues();
      current.suite = suite;
      current.indexMode = this.indexMode();
      current.isBeforeInvoked = false;
      this.setCurrent(current);
      if (storeAsLastSuite) { this.lastSelectedSuite(suite); }

      // Invoke before handlers.
      this.invokeBeforeHandlers(suite);

      // If the last invoked spec on the suite contained a load.
      let lastInvokedSpec = this.lastInvokedSpec(suite);
      if (lastInvokedSpec && lastInvokedSpec.spec && lastInvokedSpec.isLoader) {
        this.invokeSpec(lastInvokedSpec.spec);
      }
    }

    // Finish up.
    return this;
  }

  /**
   * Invokes the [before] handlers for
   * the given suite if required.
   * @return {boolean}  - true if the handlers were invoked
   *                    - false if they have already been invoked.
   */
  invokeBeforeHandlers(suite) {
    if (this.current.get("isBeforeInvoked")) { return false; }
    const self = suite.meta.thisContext;
    suite.beforeHandlers.invoke(self);
    this.current = this.current.set("isBeforeInvoked", true);
    return true
  }

  /**
   * Invokes the given spec.
   * @param spec: The [Spec] to invoke.
   * @param callback: Invoked upon completion.
   *                   Immediately if the spec is not asynchronous.
   */
  invokeSpec(spec, callback) {
    // Setup initial conditions.
    const suite = spec.parentSuite;
    const self = suite.meta.thisContext;
    this.invokeBeforeHandlers(suite)
    let loadInvokeCountBefore = this.loadInvokeCount;

    // Invoke the methods.
    spec.invoke(self, callback);

    // Store a reference to last-invoked spec.
    this.lastInvokedSpec(suite, {
      spec: spec,
      isLoader: (this.loadInvokeCount > loadInvokeCountBefore)
    });

    // Increment the current invoke count for the spec.
    let specInvokeCount = this.current.get("specInvokeCount") || {};
    let total = specInvokeCount[spec.id] || 0;
    specInvokeCount[spec.id] = total + 1;
    this.setCurrent({ specInvokeCount: specInvokeCount });

    // Finish up.
    return this;
  }

  /**
   * Invokes a spec on the server.
   * @param spec: The [Spec] to invoke.
   * @param callback: Invoked upon completion.
   */
  invokeServerSpec(spec, callback) {
    const server = rest();
    server.onReady(() => {
        server.methods.invokeSpec.put(spec.id)
        .then((result) => {
          // TODO:
          console.log("result", result);
        });
    });
  }

  /**
   * Gets or sets the last selected [Suite].
   */
  lastSelectedSuite(suite) {
    if (suite) { suite = suite.id; }
    const result = this.localStorage("lastSelectedSuite", suite);
    return bdd.suites[result];
  }

  /**
   * Gets or sets the last spec for the given suite
   * that was invoked that had a `.load()` call within it.
   */
  lastInvokedSpec(suite, { spec, isLoader = false} = {}) {
    const KEY = `lastInvokedSpec:${ suite.id }`;
    let value;
    if (spec !== undefined) {
      // WRITE.
      value = { spec: spec.id, isLoader: isLoader }
      spec = spec.id;
    }

    // READ.
    let result = this.localStorage(KEY, value);
    if (result) {
      result.spec = R.find(spec => spec.id === result.spec, suite.specs);
    }
    return result
  }

  /**
   * Gets or sets the display mode of the left-hand index.
   * @param {string} mode: tree|suite
   */
  indexMode(mode) {
    let result = this.localStorage("indexMode", mode, { default: "tree" });
    if (mode !== undefined) {
      // WRITE (store in current state).
      this.setCurrent({ indexMode:mode });
    }

    // READ.
    result = result || "tree";
    if (result !== "tree" && this.current.get("suite") === undefined) {
      result = "tree"
    }
    return result;
  }

  /**
   * Updates the current state with the given values.
   *     NOTE: These values are cumulatively added to the state.
   *           Use "reset" to clear the state.
   *
   * @param args:  An object containing they [key:values] to set
   *               or null to clear values.
   */
  setCurrent(args) {
    // Update the state object.
    if (args) {
      Object.keys(args).forEach(key => {
        const value = args[key];
        this.current = value === undefined
                ? this.current.remove(key)
                : this.current.set(key, args[key])
      });
    } else {
      this.current = this.current.clear();
    }

    // Apply to the <Shell>.
    if (this.shell) { this.shell.setState({ current: this.current }); }
    return this;
  };

  /**
   * Logs a value to the output.
   * @param {array} values: The value or values to append.
   */
  log(...values) {
    values = R.flatten(values);
    const item = { time: new Date(), values };
    this[LOG_LIST] = this[LOG_LIST].push(item);
    this.setCurrent({ log: this[LOG_LIST], showLog: true });
    return this;
  }

  /**
   * Clears the output log.
   */
  clearLog() {
    // console.log("clear log");
    this[LOG_LIST] = this[LOG_LIST].clear();
    this.setCurrent({ log: this[LOG_LIST], showLog: false });

  }

  /**
   * Provides common access to localStorage.
   * @param key:         The unique identifier of the value (this is prefixed with the namespace).
   * @param value:       (optional). The value to set (pass null to remove).
   * @param options:
   *           default:  (optional). The default value to return if the session does not contain the value (ie. undefined).
   *
   * @return the read value.
   */
  localStorage(key, value, options) {
    return localStorage.prop(`ui-harness:${ key }`, value, options);
  }
}

// Singleton instance.
export default new Api();
<|endoftext|>