<commit_before>import {
	eventName,
	underscoredToCamelCase,
	DEFAULT_REDUX_EVENT_NAMING,
	generateReduxNamespace,
	generateReduxEventName
} from './naming'

import {
	RESULT_ACTION_PROPERTY,
	ERROR_ACTION_PROPERTY
} from './middleware/asynchronous'

// Sometimes modules for one project are imported from another project directory.
// For example, Redux actions from one project may be imported into another project.
// In such cases `Counter` is not enough because Redux event names would collide
// because actions from the first project use different `node_modules` and therefore
// use different `react-pages` modules having their own `Counter`.
// In such cases it's mandatory to pass Redux event name to `.action()` and `.simpleAction()`.
// Otherwise there will be Redux event name collisions.
// Importing packages from two different `node_modules` is not a good practice.

// Deprecated. Use `new ReduxModule()` instead.
export function createReduxModule(namespace, settings)
{
	return new ReduxModule(namespace, settings)
}

export default class ReduxModule
{
	handlers = {}
	registered_state_properties = []

	constructor(namespace = generateReduxNamespace(counter.next()), settings = {})
	{
		this.namespace = namespace

		this.settings = {
			reduxEventNaming: settings.reduxEventNaming || DEFAULT_REDUX_EVENT_NAMING,
			reduxPropertyNaming: settings.reduxPropertyNaming || underscoredToCamelCase
		}
	}

	replace(event, handler)
	{
		if (!Array.isArray(handler))
		{
			handler = [handler]
		}

		this.handlers[event] = handler
	}

	on(namespace, event, handler)
	{
		if (typeof event === 'function')
		{
			handler = event
			event = namespace
			namespace = undefined
		}
		else
		{
			// Use "success" event name.
			event = this.settings.reduxEventNaming(eventName(namespace, event))[1]
		}

		if (!this.handlers[event])
		{
			this.handlers[event] = []
		}

		this.handlers[event].push(handler)
	}

	action(event, action, result, options)
	{
		if (event && typeof event !== 'string') {
			options = result
			result = action
			action = event
			event = undefined
		}

		// Autogenerate `event` name.
		if (!event) {
			event = generateReduxEventName(counter.next())
		}

		options = options || {}

		if (typeof action !== 'function') {
			throw new Error('[react-pages] One must pass an `action()` argument (the second one) to Redux module action creator: `reduxModule(event, action, result, options = {})`.')
		}

		return createAction(event, action, result, options, this)
	}

	simpleAction(event, action, result, options)
	{
		if (event && typeof event !== 'string') {
			options = result
			result = action
			action = event
			event = undefined
		}
		// `action` argument is currently a useless one:
		// taking arguments and converting them into an object.
		// This feature substitutes a missing `action` argument with a passthrough.
		// Deprecated: remove `action` argument in some future major version.
		if (action) {
			if (typeof result !== 'string' && typeof result !== 'function') {
				options = result
				result = action
				action = object => object
			}
		}
		options = options || {}
		options.sync = true
		return this.action(event, action, result, options)
	}

	// Returns Redux action creator for resetting error.
	resetError(event)
	{
		const
		[
			pending_event_name,
			success_event_name,
			error_event_name
		]
		= this.settings.reduxEventNaming(event)

		// Redux "action creator"
		return () =>
		({
			type  : eventName(this.namespace, error_event_name),
			error : undefined
		})
	}

	// Deprecated.
	getProperties = (state) =>
	{
		const properties = {}

		for (const property_name of this.registered_state_properties)
		{
			properties[property_name] = state[property_name]
		}

		return properties
	}

	createReducer(initialState = {}) {
		// Applies a handler based on the Redux action `type`.
		// (is copy & paste'd for all action response handlers)
		return (state = initialState, event = {}) => {
			try {
				for (const handler of (this.handlers[event.type] || [])) {
					state = handler(state, this.getEventData(event))
				}
				return state
			} catch (error) {
				// For some strange reason Redux didn't report
				// these errors to the console, hence the manual `console.error`.
				console.error(error)
				throw error
			}
		}
	}

	reducer(initialState = {}) {
		const reducer = this.createReducer(initialState)
		// The event names list is used for autogenerated
		// event names conflict detection.
		// See `checkForAutogeneratedEventNameCollision()`.
		reducer._react_pages_events = Object.keys(this.handlers)
		return reducer
	}

	getEventData(event) {
		if (Object.prototype.hasOwnProperty.call(event, RESULT_ACTION_PROPERTY)) {
			return event[RESULT_ACTION_PROPERTY]
		} else if (event[ERROR_ACTION_PROPERTY] !== undefined) {
			return event[ERROR_ACTION_PROPERTY]
		} else {
			return event
		}
	}

	add_state_properties() {
		this.registered_state_properties.push.apply(this.registered_state_properties, arguments)
	}

	// Public alias.
	properties() {
		this.add_state_properties.apply(this, arguments)
	}

	// Public alias.
	property() {
		this.add_state_properties.apply(this, arguments)
	}
}

// Returns Redux action creator.
function createAction(event, action, result, options, redux) {
	const namespace = redux.namespace
	const { sync } = options

	// If `result` is a property name,
	// then add that property to `connectXxx()`.
	if (typeof result === 'string') {
		redux.add_state_properties(result)
	}
	// If `result` is an object of property getters,
	// then add those properties to `connectXxx()`.
	else if (typeof result === 'object') {
		redux.add_state_properties(...Object.keys(result))
	}

	// Default "on result" handler is a reducer that does nothing.
	// Actually, I guess it should throw an error instead.
	// Deprecated: throw an error in some future major version
	// instead of using a "no op" reducer.
	result = result || (state => state)

	// Synchronous action
	if (sync) {
		// Reducer
		redux.on(eventName(namespace, event), get_action_value_reducer(result))
		// Redux "action creator"
		return (...parameters) => ({
			type : eventName(namespace, event),
			[RESULT_ACTION_PROPERTY] : action.apply(this, parameters)
		})
	}

	// Asynchronous action

	// Add Redux reducers handling events:
	//
	//   * pending
	//   * success
	//   * error
	//
	add_asynchronous_action_reducers(redux, namespace, event, get_action_value_reducer(result))

	// Redux "action creator"
	return (...parameters) => ({
		event: eventName(namespace, event),
		// `dispatch` and `getState` arguments are deprecated
		// and will be removed in some future major version release.
		promise: (http, dispatch, getState) => action.apply(this, parameters)(http)
	})
}

// Adds handlers for:
//
//   * pending
//   * done
//   * failed
//   * reset error
//
function add_asynchronous_action_reducers(redux, namespace, event, result_reducer)
{
	const
	[
		pending_event_name,
		success_event_name,
		error_event_name
	]
	= redux.settings.reduxEventNaming(event)

	const pending_property_name = redux.settings.reduxPropertyNaming(pending_event_name)
	const error_property_name   = redux.settings.reduxPropertyNaming(error_event_name)

	// This info will be used in `storeConnector`
	redux.add_state_properties(pending_property_name, error_property_name)

	// When Promise is created: reset result variable, clear `error`, set `pending` flag.
	redux.on(eventName(namespace, pending_event_name), (state) =>
	{
		const new_state =
		{
			...state,
			// Set `pending` flag
			[pending_property_name]: true
		}

		// Clear `error`
		if (redux.v2) {
			// For gradual migration from version "2.x" syntax.
			new_state[error_property_name] = undefined
		} else {
			delete new_state[error_property_name]
		}

		return new_state
	})

	// When Promise succeeds: clear `pending` flag, set result variable.
	redux.on(eventName(namespace, success_event_name), (state, result) =>
	{
		const new_state = result_reducer(state, result)

		// Clear `pending` flag
		if (redux.v2) {
			// For gradual migration from version "2.x" syntax.
			new_state[pending_property_name] = false
		} else {
			delete new_state[pending_property_name]
		}

		return new_state
	})

	// When Promise fails, clear `pending` flag and set `error`.
	// Can also clear `error` when no `error` is passed as part of an action.
	redux.on(eventName(namespace, error_event_name), (state, error) =>
	{
		const new_state =
		{
			...state,
			[error_property_name] : error
		}

		// Clear `pending` flag
		if (redux.v2) {
			// For gradual migration from version "2.x" syntax.
			new_state[pending_property_name] = false
		} else {
			delete new_state[pending_property_name]
			// `resetError()`
			if (!error) {
				delete new_state[error_property_name]
			}
		}

		return new_state
	})
}

// Returns a function
function get_action_value_reducer(reducer)
{
	// If `reducer` is a property name,
	// then the reducer will write action value
	// to that property of Redux state.
	if (typeof reducer === 'string')
	{
		return (state, value) =>
		({
			...state,
			[reducer]: value
		})
	}

	// If `reducer` is an object of property getters
	// then those properties will be added to Redux state.
	if (typeof reducer === 'object')
	{
		return (state, value) =>
		{
			const updated_properties = {}

			for (const property of Object.keys(reducer))
			{
				updated_properties[property] = reducer[property](value)

				// Don't know why did I previously write it like:
				// updated_properties =
				// {
				// 	...updated_properties,
				// 	...reducer[property](value)
				// }
			}

			return {
				...state,
				...updated_properties
			}
		}
	}

	// Otherwise `reducer` is `(state, value) => ...`
	return reducer
}

class Counter
{
	counter = 0

	next()
	{
		if (this.counter < MAX_SAFE_INTEGER) {
			this.counter++
		} else {
			this.counter = 1
		}
		return this.counter
	}
}

const counter = new Counter()

// `MAX_SAFE_INTEGER` is not supported by older browsers
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1
<commit_msg>ReduxModule refactored; `v2: true` deprecations removed<commit_after>import {
	eventName,
	underscoredToCamelCase,
	DEFAULT_REDUX_EVENT_NAMING,
	generateReduxNamespace,
	generateReduxEventName
} from './naming'

import {
	RESULT_ACTION_PROPERTY,
	ERROR_ACTION_PROPERTY
} from './middleware/asynchronous'

// Sometimes modules for one project are imported from another project directory.
// For example, Redux actions from one project may be imported into another project.
// In such cases `Counter` is not enough because Redux event names would collide
// because actions from the first project use different `node_modules` and therefore
// use different `react-pages` modules having their own `Counter`.
// In such cases it's mandatory to pass Redux event name to `.action()` and `.simpleAction()`.
// Otherwise there will be Redux event name collisions.
// Importing packages from two different `node_modules` is not a good practice.

// Deprecated. Use `new ReduxModule()` instead.
export function createReduxModule(namespace, settings) {
	return new ReduxModule(namespace, settings)
}

export default class ReduxModule {
	handlers = {}
	registered_state_properties = []

	constructor(namespace = generateReduxNamespace(counter.next()), settings = {}) {
		this.namespace = namespace
		this.settings = {
			reduxEventNaming: settings.reduxEventNaming || DEFAULT_REDUX_EVENT_NAMING,
			reduxPropertyNaming: settings.reduxPropertyNaming || underscoredToCamelCase
		}
	}

	replace(event, handler) {
		if (!Array.isArray(handler)) {
			handler = [handler]
		}
		this.handlers[event] = handler
	}

	on(namespace, event, handler) {
		if (typeof event === 'function') {
			handler = event
			event = namespace
			namespace = undefined
		} else {
			// Use "success" event name.
			event = this.settings.reduxEventNaming(eventName(namespace, event))[1]
		}
		if (!this.handlers[event]) {
			this.handlers[event] = []
		}
		this.handlers[event].push(handler)
	}

	action(event, action, result, options) {
		if (event && typeof event !== 'string') {
			options = result
			result = action
			action = event
			event = undefined
		}
		// Autogenerate `event` name.
		if (!event) {
			event = generateReduxEventName(counter.next())
		}
		options = options || {}
		if (typeof action !== 'function') {
			throw new Error('[react-pages] One must pass an `action()` argument (the second one) to Redux module action creator: `reduxModule(event, action, result, options = {})`.')
		}
		return createAction(event, action, result, options, this)
	}

	simpleAction(event, action, result, options) {
		if (event && typeof event !== 'string') {
			options = result
			result = action
			action = event
			event = undefined
		}
		// `action` argument is currently a useless one:
		// taking arguments and converting them into an object.
		// This feature substitutes a missing `action` argument with a passthrough.
		// Deprecated: remove `action` argument in some future major version.
		if (action) {
			if (typeof result !== 'string' && typeof result !== 'function') {
				options = result
				result = action
				action = object => object
			}
		}
		options = options || {}
		options.sync = true
		return this.action(event, action, result, options)
	}

	createReducer(initialState = {}) {
		// Applies a handler based on the Redux action `type`.
		// (is copy & paste'd for all action response handlers)
		return (state = initialState, event = {}) => {
			try {
				for (const handler of (this.handlers[event.type] || [])) {
					state = handler(state, this.getEventData(event))
				}
				return state
			} catch (error) {
				// For some strange reason Redux didn't report
				// these errors to the console, hence the manual `console.error`.
				console.error(error)
				throw error
			}
		}
	}

	reducer(initialState = {}) {
		const reducer = this.createReducer(initialState)
		// The event names list is used for autogenerated
		// event names conflict detection.
		// See `checkForAutogeneratedEventNameCollision()`.
		reducer._react_pages_events = Object.keys(this.handlers)
		return reducer
	}

	getEventData(event) {
		if (Object.prototype.hasOwnProperty.call(event, RESULT_ACTION_PROPERTY)) {
			return event[RESULT_ACTION_PROPERTY]
		} else if (event[ERROR_ACTION_PROPERTY] !== undefined) {
			return event[ERROR_ACTION_PROPERTY]
		} else {
			return event
		}
	}

	add_state_properties() {
		this.registered_state_properties.push.apply(this.registered_state_properties, arguments)
	}

	// Public alias.
	properties() {
		this.add_state_properties.apply(this, arguments)
	}

	// Public alias.
	property() {
		this.add_state_properties.apply(this, arguments)
	}
}

// Returns Redux action creator.
function createAction(event, action, result, options, redux) {
	const namespace = redux.namespace
	const { sync } = options

	// If `result` is a property name,
	// then add that property to `connectXxx()`.
	if (typeof result === 'string') {
		redux.add_state_properties(result)
	}
	// If `result` is an object of property getters,
	// then add those properties to `connectXxx()`.
	else if (typeof result === 'object') {
		redux.add_state_properties(...Object.keys(result))
	}

	// Default "on result" handler is a reducer that does nothing.
	// Actually, I guess it should throw an error instead.
	// Deprecated: throw an error in some future major version
	// instead of using a "no op" reducer.
	result = result || (state => state)

	// Synchronous action
	if (sync) {
		// Reducer
		redux.on(eventName(namespace, event), get_action_value_reducer(result))
		// Redux "action creator"
		return (...parameters) => ({
			type : eventName(namespace, event),
			[RESULT_ACTION_PROPERTY] : action.apply(this, parameters)
		})
	}

	// Asynchronous action

	// Add Redux reducers handling events:
	//
	//   * pending
	//   * success
	//   * error
	//
	add_asynchronous_action_reducers(redux, namespace, event, get_action_value_reducer(result))

	// Redux "action creator"
	return (...parameters) => ({
		event: eventName(namespace, event),
		// `dispatch` and `getState` arguments are deprecated
		// and will be removed in some future major version release.
		promise: (http, dispatch, getState) => action.apply(this, parameters)(http)
	})
}

// Adds handlers for:
//
//   * pending
//   * done
//   * failed
//   * reset error
//
function add_asynchronous_action_reducers(redux, namespace, event, result_reducer) {
	const
	[
		pending_event_name,
		success_event_name,
		error_event_name
	]
	= redux.settings.reduxEventNaming(event)

	const pending_property_name = redux.settings.reduxPropertyNaming(pending_event_name)
	const error_property_name   = redux.settings.reduxPropertyNaming(error_event_name)

	// This info will be used in `storeConnector`
	redux.add_state_properties(pending_property_name, error_property_name)

	// When Promise is created: reset result variable, clear `error`, set `pending` flag.
	redux.on(eventName(namespace, pending_event_name), (state) => {
		state = {
			...state,
			// Set `pending` flag
			[pending_property_name]: true
		}
		// Clear `error`
		delete state[error_property_name]
		return state
	})

	// When Promise succeeds: clear `pending` flag, set result variable.
	redux.on(eventName(namespace, success_event_name), (state, result) => {
		state = result_reducer(state, result)
		delete state[pending_property_name]
		return state
	})

	// When Promise fails, clear `pending` flag and set `error`.
	// Can also clear `error` when no `error` is passed as part of an action.
	redux.on(eventName(namespace, error_event_name), (state, error) => {
		state = {
			...state,
			[error_property_name]: error
		}
		// Clear `pending` flag
		delete state[pending_property_name]
		return state
	})
}

// Returns a function
function get_action_value_reducer(reducer) {
	// If `reducer` is a property name,
	// then the reducer will write action value
	// to that property of Redux state.
	if (typeof reducer === 'string') {
		return (state, value) => ({
			...state,
			[reducer]: value
		})
	}

	// If `reducer` is an object of property getters
	// then those properties will be added to Redux state.
	if (typeof reducer === 'object') {
		return (state, value) => {
			const updated_properties = {}
			for (const property of Object.keys(reducer)) {
				updated_properties[property] = reducer[property](value)
				// Don't know why did I previously write it like:
				// updated_properties =
				// {
				// 	...updated_properties,
				// 	...reducer[property](value)
				// }
			}
			return {
				...state,
				...updated_properties
			}
		}
	}

	// Otherwise `reducer` is `(state, value) => ...`
	return reducer
}

class Counter {
	counter = 0
	next() {
		if (this.counter < MAX_SAFE_INTEGER) {
			this.counter++
		} else {
			this.counter = 1
		}
		return this.counter
	}
}

const counter = new Counter()

// `MAX_SAFE_INTEGER` is not supported by older browsers
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1
<|endoftext|>


<commit_before>
const EXPORT = ['CommentViewer'];

// local utility
this.__defineGetter__('aWin', function() getTopWin());
this.__defineGetter__('aDoc', function() getTopWin().gBrowser.contentDocument);
this.__defineGetter__('isHttp', function() aDoc && aDoc.location.protocol.indexOf('http') == 0);

let E = createElementBindDocument(document, XHTML_NS);

elementGetter(this, 'panelComment', 'hBookmark-panel-comment', document);
elementGetter(this, 'commentButton', 'hBookmark-status-comment', document);
elementGetter(this, 'commentContainer', 'hBookmark-comment-container', document);
elementGetter(this, 'listContainer', 'hBookmark-comment-list-container', document);
elementGetter(this, 'list', 'hBookmark-comment-list', document);
elementGetter(this, 'listDiv', 'hBookmark-comment-div', document);
elementGetter(this, 'statusbar', 'status-bar', document);

elementGetter(this, 'faviconImage', 'hBookmark-comment-favicon', document);
elementGetter(this, 'titleLabel', 'hBookmark-comment-title', document);
elementGetter(this, 'usersLabel', 'hBookmark-comment-users', document);
elementGetter(this, 'usersPubPriLabel', 'hBookmark-comment-pub-pri-users', document);
elementGetter(this, 'toggleImage', 'hBookmark-comment-toggle', document);

let userIcon = function(username) {
    return E('img', {
        src: UserUtils.getProfileIcon(username),
        className: 'usericon',
        width: '16px',
        height: '16px',
        alt: username,
    });
}

var CommentViewer = {
    filterToggle: function() {
        CommentViewer.prefs.set('showAll', !CommentViewer.prefs.get('showAll'));
        CommentViewer.updateToggle();
        CommentViewer.updateViewer(null, true);
    },
    get prefs() {
        if (!CommentViewer._prefs) {
            CommentViewer._prefs = new Prefs('extensions.hatenabookmark.commentviewer.');
        }
        return CommentViewer._prefs;
    },
    updateToggle: function() {
        CommentViewer.updateImage(CommentViewer.isFilter);
    },
    updateImage: function(filterd) {
        let src;
        if (filterd) {
             src = "chrome://hatenabookmark/skin/images/comment-viewer-toggle-off.png";
        } else {
             src = "chrome://hatenabookmark/skin/images/comment-viewer-toggle-on.png";
        }
        toggleImage.src = src;
    },
    get isFilter() {
        return !CommentViewer.prefs.get('showAll');
    },
    showClick: function CommentViewer_show() {
        if (panelComment.state == 'closed')
            CommentViewer.show();
    },
    show: function CommentViewer_show(url) {
        if (!url) {
            if (!isHttp) return;
            url = aDoc.location.href;
        }
        commentButton.setAttribute('loading', 'true');
        var self = this;
        HTTPCache.comment.async_get(url, function(data) {
            if (!data || !data.title) {
                data = {};
                data.url = data.title = url;
                data.bookmarks = [];
            }
            data.favicon = favicon(data.url);
            self.updateComment(data);
        });
    },
    toggle: function CommentViewer_toggle(url) {
        if (!url && isHttp) {
            url = aDoc.location.href;
        }

        if (panelComment.state != 'close' && url != CommentViewer.currentURL) {
            CommentViewer.show(url);
        } else {
            CommentViewer.hide();
        }
    },
    autoHoverShow: function() {
        if (!CommentViewer.prefs.get('autoHoverShow')) return;

        let url;
        if (!url && isHttp)
            url = aDoc.location.href;

        if (!HTTPCache.counter.isValid(url)) {
            // valid でない URL なら、自動コメント機能を無効に
            return;
        }

        if (isHttp && HTTPCache.counter.has(url)) {
            let count = HTTPCache.counter.get(url);
            if (!(count > 0)) {
                // カウントキャッシュがあった場合、チェックして、ある場合のみ表示
                return;
            }
        }

        if (panelComment.state != 'close' && url != CommentViewer.currentURL)
            CommentViewer.show(url);
    },
    currentURL: null,
    updateComment: function CommentViewer_updateComment(data) {
        CommentViewer.currentURL = data.url;
        // 非表示ユーザをフィルター
        if (User.user) {
            let regex = User.user.ignores;
            if (regex) {
                data.bookmarks = data.bookmarks.filter(function(b) !regex.test(b.user));
            }
        }
        data.publicCount = data.bookmarks.length;
        data.privateCount = data.count - data.publicCount;
        panelComment.setAttribute('hTransparent', true);
        panelComment.openPopup(statusbar, 'before_end', -20, 0,false,false);
        CommentViewer.updateViewer(data);
        commentButton.setAttribute('loading', 'false');
    },
    renderComment: function(bookmarks, limit, fragment) {
        if (!fragment)
            fragment = document.createDocumentFragment();
        let i = 0;
        let len = bookmarks.length;
        let B_URL = 'http://b.hatena.ne.jp/';
        let isFilter = CommentViewer.isFilter;
        let eid = bookmarks.eid;
        let URLRegex = new RegExp("\\b((?:http|https|ftp)://[A-Za-z0-9~/._\?\&=\\-%#\+:\;,\@\']+)", 'g');
        while (i++ < Math.min(limit, len)) {
            let b = bookmarks.shift();
            let li = E('li');
            let userlink = B_URL + b.user + '/';
            let ymd = b.timestamp.split(' ')[0];
            let permalink = userlink + ymd.replace(/\//g, '') + '#bookmark-' +  eid;
            let icon = userIcon(b.user);
            li.appendChild(icon);
            let a;
            li.appendChild(a = E('a', {href: permalink}, b.user));
            a.className = 'username';
            if (!isFilter && b.tags) b.tags.forEach(function(tag, index) {
                let userlinkTag = userlink + encodeURIComponent(tag);
                if (index) li.appendChild(document.createTextNode(', '));
                li.appendChild(a = E('a', {href: userlinkTag}, tag));
                a.className = 'tag';
            });
            li.appendChild(a = E('span'));
            a.innerHTML = b.comment.replace(/&/g, '&').
                   replace(/</g, '<').
                   replace(/>/g, '>').
                   replace(/\"/g, '"').
                   replace(URLRegex, function(m) {
                return '' + m + '';
            });
            a.className = 'comment'
            li.appendChild(a = E('span', {}, ymd));
            a.className = 'timestamp';
            fragment.appendChild(li);
        }
        return fragment;
    },
    updateViewer: function (data, ignoreAutoFilter) {
        if (!data) {
            data = CommentViewer.lastData;
        } else {
            CommentViewer.lastData = data;
        }

        let autoFilter = CommentViewer.autoFilter;
        if (autoFilter && !ignoreAutoFilter) {
            // 閾値以下なら、Filter しない
            if (data.bookmarks.length < autoFilter) {
                CommentViewer.prefs.set('showAll', true);
                CommentViewer.updateImage(false);
            } else {
                CommentViewer.prefs.set('showAll', false);
                CommentViewer.updateImage(true);
            }
        }

        let isFilter = CommentViewer.isFilter;
        if (CommentViewer.lastRenderData[0] == data.url && CommentViewer.lastRenderData[1] == isFilter) {
            // data.url が同じ、かつ filter してない
            panelComment.removeAttribute('hTransparent');
            return;
        }
        while(list.firstChild) list.removeChild(list.firstChild);

        let bookmarks = Array.slice(data.bookmarks);
        bookmarks.eid = data.eid;

        if (isFilter) {
            bookmarks = bookmarks.filter(function(b) b.comment.length);
        }
        let fragment = document.createDocumentFragment();
        if (bookmarks.length) {
            CommentViewer.renderComment(bookmarks, 30, fragment);
            if (bookmarks.length) {
                // まだレンダリングする項目があれば、非同期レンダリングをする
                CommentViewer.lazyWriter.reset();
                CommentViewer.lazyWriterBookmarks = bookmarks;
                CommentViewer.lazyWriter.start();
            }
        } else {
             let li = E('li', {} , UIEncodeText('表示できるブックマークコメントはありません。'));
             li.className = 'notice';
             fragment.appendChild(li);
        }
        list.appendChild(fragment);
        faviconImage.src = data.favicon;
        titleLabel.value = decodeReferences(data.title) || data.url;
        let c = data.count;
        if (c) {
            usersLabel.value = parseInt(data.count) == 0 ? (data.count + ' user') :  data.count + ' users';
            if (data.privateCount) {
                usersPubPriLabel.value = '(' + data.publicCount + ' + ' + data.privateCount + ')';
            } else {
                usersPubPriLabel.value = '';
            }
        } else {
            usersLabel.value = '';
            usersPubPriLabel.value = '';
        }
        setTimeout(function() {
             CommentViewer.updatePosition();
        }, 0);
        CommentViewer.lastRenderData = [data.url, isFilter];
    },
    lastRenderData: [],
    updatePosition: function() {
        let height = list.offsetHeight + 30;
        let h, w;
        if (CommentViewer.prefs.get('autoResize')) {
            let size = Math.max(300, Math.min(window.content.innerWidth - 50, window.content.innerHeight - 50));
            h = w = parseInt(size);
            h = Math.min(h, height);
        } else {
            h = Math.min(CommentViewer.viewerMaxHeight, height);
            w = Math.min(CommentViewer.viewerWidth, window.content.innerWidth - 10);
        }
        p('comment viewer pos:' + h + ', ' + w);
        listContainer.style.height = '' + h + 'px';
        commentContainer.style.width = '' + w + 'px';
        panelComment.removeAttribute('hTransparent');
        setTimeout(function() {
            listDiv.focus()
        }, 10);
    },
    get viewerMaxHeight() {
        return CommentViewer.prefs.get('height');
    },
    get viewerWidth() {
        return CommentViewer.prefs.get('width');
    },
    get autoFilter() {
        if (CommentViewer.prefs.get('autoFilter')) {
            return CommentViewer.prefs.get('autoFilterThreshold');
        } else {
            return false;
        }
    },
    get hideAfterTimer() {
        if (!CommentViewer._hideAfter) {
            let hideAfter = new Timer(100, 1);
            CommentViewer._hideAfter = hideAfter;
        }
        return CommentViewer._hideAfter;
    },
    hide: function CommentViewer_hide() {
        if (panelComment.state != 'closed') {
            panelComment.hidePopup();
        }
    },
    popupHiddenHandler: function(ev) {
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandler, false);
        gBrowser.removeEventListener('mouseover', CommentViewer.browserOverHandler, false);
        window.removeEventListener('mouseout', CommentViewer.windowMouseOutHandler, false);
        CommentViewer.lazyWriter.stop();
        CommentViewer.hideTimer.stop();
        CommentViewer.currentURL = null;
        CommentViewer.lastData = null;
        CommentViewer.hideAfterTimer.reset();
        CommentViewer.hideAfterTimer.start();
    },
    popupShownHandler: function(ev) {
        CommentViewer.hideTimer.stop();
        panelComment.addEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        panelComment.addEventListener('mouseover', CommentViewer.popupOverHandler, false);
        window.addEventListener('mouseout', CommentViewer.windowMouseOutHandler, false);
    },
    get lazyWriter() {
        if (!CommentViewer._lazyTimer) {
            let lazyTimer = new Timer(50);
            lazyTimer.createListener('timer', CommentViewer.lazyTimerHandler);
            CommentViewer._lazyTimer = lazyTimer;
        }
        return CommentViewer._lazyTimer;
    },
    lazyTimerHandler: function(ev) {
        let bs = CommentViewer.lazyWriterBookmarks;
        if (bs && bs.length) {
            let fragment = CommentViewer.renderComment(bs, 50, fragment);
            list.appendChild(fragment);
        }
    },
    windowMouseOutHandler: function(ev) {
        if (!ev.relatedTarget) {
            CommentViewer.popupMouseoutHandler(ev);
        }
    },
    popupOverHandlerFirst: function(ev) {
        /*
         * first を作ってるのは、一度コメントビュワーのパネルにマウスオーバーしてから
         * hidden を有効にするため。これをしないと、右クリック拡張からコメントビュワー表示時に
         * マウスを少し動かすだけで消えるようになってしまう
         */
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        gBrowser.addEventListener('mouseover', CommentViewer.browserOverHandler, false);
    },
    popupOverHandler: function(ev) {
        CommentViewer.hideTimer.stop();
    },
    get hideTimer() {
        if (!CommentViewer._hideTimer) {
            let hideTimer = new Timer(300, 1);
            hideTimer.createListener('timerComplete', CommentViewer.hideTimerCompleteHandler);
            CommentViewer._hideTimer = hideTimer;
        }
        return CommentViewer._hideTimer;
    },
    hideTimerCompleteHandler: function(ev) {
        CommentViewer.hide();
    },
    popupMouseoutHandler: function(ev) {
        if (CommentViewer.prefs.get('autoHoverShow')) {
            let hideTimer = CommentViewer.hideTimer;
            let t = ev.currentTarget;
            let rel = ev.relatedTarget;
            while( rel ) {
                if (rel == t) {
                    hideTimer.stop();
                    return;
                }
                rel = rel.parentNode;
            }
            hideTimer.reset();
            hideTimer.start();
        }
    },
    browserOverHandler: function(ev) {
        if (CommentViewer.prefs.get('autoHoverShow')) {
            let hideTimer = CommentViewer.hideTimer;
            hideTimer.reset();
            hideTimer.start();
        }
    },
    buttonClickHandler: function(ev) {
        p(['bclick', ev.button]);
        if (ev.button == 2) return; // 右クリック

        if (CommentViewer.hideAfterTimer.running) {
            /*
             * XXX:
             * コメントビュワーの popup が autohide によって、クリックされると閉じられるため
             * タイマーをチェックし、
             * 非表示にされた直後なら、なにもしない
             */
        } else {
            CommentViewer.toggle();
        }
    },
    loadHandler: function CommentViewer_loadHandler() {
        commentButton.addEventListener('mousedown', CommentViewer.buttonClickHandler, true);
        panelComment.addEventListener('popuphidden', CommentViewer.popupHiddenHandler, false);
        panelComment.addEventListener('popupshown', CommentViewer.popupShownHandler, false);
        // panelComment.addEventListener('mouseout', CommentViewer.popupMouseoutHandler, false);
        listDiv.addEventListener('click', CommentViewer.listClickHandler, true);
        CommentViewer.updateToggle();
    },
    listClickHandler: function CommentViewer_listClickHandler(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        if (ev.target.alt == 'close') {
            CommentViewer.hide();
            return;
        }
        let link = CommentViewer.getHref(ev.target);
        if (link) {
            hOpenUILink(link, ev);
        }
    },
    goEntry: function(ev) {
        let url = CommentViewer.currentURL || CommentViewer.lastRenderData[0];
        hOpenUILink(entryURL(url), ev);
    },
    getHref: function (el) {
        if (typeof el.href != 'undefined') {
            return el.href;
        } else if (el.parentNode) {
            return CommentViewer.getHref(el.parentNode);
        } else {
            return null;
        }
    },
}

EventService.createListener('load', CommentViewer.loadHandler);

<commit_msg>コメントビューワで二重引用符もURLの一部とみなされてしまったのを修正。<commit_after>
const EXPORT = ['CommentViewer'];

// local utility
this.__defineGetter__('aWin', function() getTopWin());
this.__defineGetter__('aDoc', function() getTopWin().gBrowser.contentDocument);
this.__defineGetter__('isHttp', function() aDoc && aDoc.location.protocol.indexOf('http') == 0);

let E = createElementBindDocument(document, XHTML_NS);

elementGetter(this, 'panelComment', 'hBookmark-panel-comment', document);
elementGetter(this, 'commentButton', 'hBookmark-status-comment', document);
elementGetter(this, 'commentContainer', 'hBookmark-comment-container', document);
elementGetter(this, 'listContainer', 'hBookmark-comment-list-container', document);
elementGetter(this, 'list', 'hBookmark-comment-list', document);
elementGetter(this, 'listDiv', 'hBookmark-comment-div', document);
elementGetter(this, 'statusbar', 'status-bar', document);

elementGetter(this, 'faviconImage', 'hBookmark-comment-favicon', document);
elementGetter(this, 'titleLabel', 'hBookmark-comment-title', document);
elementGetter(this, 'usersLabel', 'hBookmark-comment-users', document);
elementGetter(this, 'usersPubPriLabel', 'hBookmark-comment-pub-pri-users', document);
elementGetter(this, 'toggleImage', 'hBookmark-comment-toggle', document);

let userIcon = function(username) {
    return E('img', {
        src: UserUtils.getProfileIcon(username),
        className: 'usericon',
        width: '16px',
        height: '16px',
        alt: username,
    });
}

var CommentViewer = {
    filterToggle: function() {
        CommentViewer.prefs.set('showAll', !CommentViewer.prefs.get('showAll'));
        CommentViewer.updateToggle();
        CommentViewer.updateViewer(null, true);
    },
    get prefs() {
        if (!CommentViewer._prefs) {
            CommentViewer._prefs = new Prefs('extensions.hatenabookmark.commentviewer.');
        }
        return CommentViewer._prefs;
    },
    updateToggle: function() {
        CommentViewer.updateImage(CommentViewer.isFilter);
    },
    updateImage: function(filterd) {
        let src;
        if (filterd) {
             src = "chrome://hatenabookmark/skin/images/comment-viewer-toggle-off.png";
        } else {
             src = "chrome://hatenabookmark/skin/images/comment-viewer-toggle-on.png";
        }
        toggleImage.src = src;
    },
    get isFilter() {
        return !CommentViewer.prefs.get('showAll');
    },
    showClick: function CommentViewer_show() {
        if (panelComment.state == 'closed')
            CommentViewer.show();
    },
    show: function CommentViewer_show(url) {
        if (!url) {
            if (!isHttp) return;
            url = aDoc.location.href;
        }
        commentButton.setAttribute('loading', 'true');
        var self = this;
        HTTPCache.comment.async_get(url, function(data) {
            if (!data || !data.title) {
                data = {};
                data.url = data.title = url;
                data.bookmarks = [];
            }
            data.favicon = favicon(data.url);
            self.updateComment(data);
        });
    },
    toggle: function CommentViewer_toggle(url) {
        if (!url && isHttp) {
            url = aDoc.location.href;
        }

        if (panelComment.state != 'close' && url != CommentViewer.currentURL) {
            CommentViewer.show(url);
        } else {
            CommentViewer.hide();
        }
    },
    autoHoverShow: function() {
        if (!CommentViewer.prefs.get('autoHoverShow')) return;

        let url;
        if (!url && isHttp)
            url = aDoc.location.href;

        if (!HTTPCache.counter.isValid(url)) {
            // valid でない URL なら、自動コメント機能を無効に
            return;
        }

        if (isHttp && HTTPCache.counter.has(url)) {
            let count = HTTPCache.counter.get(url);
            if (!(count > 0)) {
                // カウントキャッシュがあった場合、チェックして、ある場合のみ表示
                return;
            }
        }

        if (panelComment.state != 'close' && url != CommentViewer.currentURL)
            CommentViewer.show(url);
    },
    currentURL: null,
    updateComment: function CommentViewer_updateComment(data) {
        CommentViewer.currentURL = data.url;
        // 非表示ユーザをフィルター
        if (User.user) {
            let regex = User.user.ignores;
            if (regex) {
                data.bookmarks = data.bookmarks.filter(function(b) !regex.test(b.user));
            }
        }
        data.publicCount = data.bookmarks.length;
        data.privateCount = data.count - data.publicCount;
        panelComment.setAttribute('hTransparent', true);
        panelComment.openPopup(statusbar, 'before_end', -20, 0,false,false);
        CommentViewer.updateViewer(data);
        commentButton.setAttribute('loading', 'false');
    },
    renderComment: function(bookmarks, limit, fragment) {
        if (!fragment)
            fragment = document.createDocumentFragment();
        let i = 0;
        let len = bookmarks.length;
        let B_URL = 'http://b.hatena.ne.jp/';
        let isFilter = CommentViewer.isFilter;
        let eid = bookmarks.eid;
        let URLRegex = new RegExp("\\b((?:http|https|ftp)://[A-Za-z0-9~/._\?\&=\\-%#\+:\;,\@\']+)", 'g');
        while (i++ < Math.min(limit, len)) {
            let b = bookmarks.shift();
            let li = E('li');
            let userlink = B_URL + b.user + '/';
            let ymd = b.timestamp.split(' ')[0];
            let permalink = userlink + ymd.replace(/\//g, '') + '#bookmark-' +  eid;
            let icon = userIcon(b.user);
            li.appendChild(icon);
            let a;
            li.appendChild(a = E('a', {href: permalink}, b.user));
            a.className = 'username';
            if (!isFilter && b.tags) b.tags.forEach(function(tag, index) {
                let userlinkTag = userlink + encodeURIComponent(tag);
                if (index) li.appendChild(document.createTextNode(', '));
                li.appendChild(a = E('a', {href: userlinkTag}, tag));
                a.className = 'tag';
            });
            li.appendChild(a = E('span'));
            a.innerHTML = b.comment.replace(/&/g, '&').
                   replace(/</g, '<').
                   replace(/>/g, '>').
                   replace(URLRegex, function(m) {
                let url = m.replace(/\"/g, '"');
                return '' + url + '';
            });
            a.className = 'comment'
            li.appendChild(a = E('span', {}, ymd));
            a.className = 'timestamp';
            fragment.appendChild(li);
        }
        return fragment;
    },
    updateViewer: function (data, ignoreAutoFilter) {
        if (!data) {
            data = CommentViewer.lastData;
        } else {
            CommentViewer.lastData = data;
        }

        let autoFilter = CommentViewer.autoFilter;
        if (autoFilter && !ignoreAutoFilter) {
            // 閾値以下なら、Filter しない
            if (data.bookmarks.length < autoFilter) {
                CommentViewer.prefs.set('showAll', true);
                CommentViewer.updateImage(false);
            } else {
                CommentViewer.prefs.set('showAll', false);
                CommentViewer.updateImage(true);
            }
        }

        let isFilter = CommentViewer.isFilter;
        if (CommentViewer.lastRenderData[0] == data.url && CommentViewer.lastRenderData[1] == isFilter) {
            // data.url が同じ、かつ filter してない
            panelComment.removeAttribute('hTransparent');
            return;
        }
        while(list.firstChild) list.removeChild(list.firstChild);

        let bookmarks = Array.slice(data.bookmarks);
        bookmarks.eid = data.eid;

        if (isFilter) {
            bookmarks = bookmarks.filter(function(b) b.comment.length);
        }
        let fragment = document.createDocumentFragment();
        if (bookmarks.length) {
            CommentViewer.renderComment(bookmarks, 30, fragment);
            if (bookmarks.length) {
                // まだレンダリングする項目があれば、非同期レンダリングをする
                CommentViewer.lazyWriter.reset();
                CommentViewer.lazyWriterBookmarks = bookmarks;
                CommentViewer.lazyWriter.start();
            }
        } else {
             let li = E('li', {} , UIEncodeText('表示できるブックマークコメントはありません。'));
             li.className = 'notice';
             fragment.appendChild(li);
        }
        list.appendChild(fragment);
        faviconImage.src = data.favicon;
        titleLabel.value = decodeReferences(data.title) || data.url;
        let c = data.count;
        if (c) {
            usersLabel.value = parseInt(data.count) == 0 ? (data.count + ' user') :  data.count + ' users';
            if (data.privateCount) {
                usersPubPriLabel.value = '(' + data.publicCount + ' + ' + data.privateCount + ')';
            } else {
                usersPubPriLabel.value = '';
            }
        } else {
            usersLabel.value = '';
            usersPubPriLabel.value = '';
        }
        setTimeout(function() {
             CommentViewer.updatePosition();
        }, 0);
        CommentViewer.lastRenderData = [data.url, isFilter];
    },
    lastRenderData: [],
    updatePosition: function() {
        let height = list.offsetHeight + 30;
        let h, w;
        if (CommentViewer.prefs.get('autoResize')) {
            let size = Math.max(300, Math.min(window.content.innerWidth - 50, window.content.innerHeight - 50));
            h = w = parseInt(size);
            h = Math.min(h, height);
        } else {
            h = Math.min(CommentViewer.viewerMaxHeight, height);
            w = Math.min(CommentViewer.viewerWidth, window.content.innerWidth - 10);
        }
        p('comment viewer pos:' + h + ', ' + w);
        listContainer.style.height = '' + h + 'px';
        commentContainer.style.width = '' + w + 'px';
        panelComment.removeAttribute('hTransparent');
        setTimeout(function() {
            listDiv.focus()
        }, 10);
    },
    get viewerMaxHeight() {
        return CommentViewer.prefs.get('height');
    },
    get viewerWidth() {
        return CommentViewer.prefs.get('width');
    },
    get autoFilter() {
        if (CommentViewer.prefs.get('autoFilter')) {
            return CommentViewer.prefs.get('autoFilterThreshold');
        } else {
            return false;
        }
    },
    get hideAfterTimer() {
        if (!CommentViewer._hideAfter) {
            let hideAfter = new Timer(100, 1);
            CommentViewer._hideAfter = hideAfter;
        }
        return CommentViewer._hideAfter;
    },
    hide: function CommentViewer_hide() {
        if (panelComment.state != 'closed') {
            panelComment.hidePopup();
        }
    },
    popupHiddenHandler: function(ev) {
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandler, false);
        gBrowser.removeEventListener('mouseover', CommentViewer.browserOverHandler, false);
        window.removeEventListener('mouseout', CommentViewer.windowMouseOutHandler, false);
        CommentViewer.lazyWriter.stop();
        CommentViewer.hideTimer.stop();
        CommentViewer.currentURL = null;
        CommentViewer.lastData = null;
        CommentViewer.hideAfterTimer.reset();
        CommentViewer.hideAfterTimer.start();
    },
    popupShownHandler: function(ev) {
        CommentViewer.hideTimer.stop();
        panelComment.addEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        panelComment.addEventListener('mouseover', CommentViewer.popupOverHandler, false);
        window.addEventListener('mouseout', CommentViewer.windowMouseOutHandler, false);
    },
    get lazyWriter() {
        if (!CommentViewer._lazyTimer) {
            let lazyTimer = new Timer(50);
            lazyTimer.createListener('timer', CommentViewer.lazyTimerHandler);
            CommentViewer._lazyTimer = lazyTimer;
        }
        return CommentViewer._lazyTimer;
    },
    lazyTimerHandler: function(ev) {
        let bs = CommentViewer.lazyWriterBookmarks;
        if (bs && bs.length) {
            let fragment = CommentViewer.renderComment(bs, 50, fragment);
            list.appendChild(fragment);
        }
    },
    windowMouseOutHandler: function(ev) {
        if (!ev.relatedTarget) {
            CommentViewer.popupMouseoutHandler(ev);
        }
    },
    popupOverHandlerFirst: function(ev) {
        /*
         * first を作ってるのは、一度コメントビュワーのパネルにマウスオーバーしてから
         * hidden を有効にするため。これをしないと、右クリック拡張からコメントビュワー表示時に
         * マウスを少し動かすだけで消えるようになってしまう
         */
        panelComment.removeEventListener('mouseover', CommentViewer.popupOverHandlerFirst, false);
        gBrowser.addEventListener('mouseover', CommentViewer.browserOverHandler, false);
    },
    popupOverHandler: function(ev) {
        CommentViewer.hideTimer.stop();
    },
    get hideTimer() {
        if (!CommentViewer._hideTimer) {
            let hideTimer = new Timer(300, 1);
            hideTimer.createListener('timerComplete', CommentViewer.hideTimerCompleteHandler);
            CommentViewer._hideTimer = hideTimer;
        }
        return CommentViewer._hideTimer;
    },
    hideTimerCompleteHandler: function(ev) {
        CommentViewer.hide();
    },
    popupMouseoutHandler: function(ev) {
        if (CommentViewer.prefs.get('autoHoverShow')) {
            let hideTimer = CommentViewer.hideTimer;
            let t = ev.currentTarget;
            let rel = ev.relatedTarget;
            while( rel ) {
                if (rel == t) {
                    hideTimer.stop();
                    return;
                }
                rel = rel.parentNode;
            }
            hideTimer.reset();
            hideTimer.start();
        }
    },
    browserOverHandler: function(ev) {
        if (CommentViewer.prefs.get('autoHoverShow')) {
            let hideTimer = CommentViewer.hideTimer;
            hideTimer.reset();
            hideTimer.start();
        }
    },
    buttonClickHandler: function(ev) {
        p(['bclick', ev.button]);
        if (ev.button == 2) return; // 右クリック

        if (CommentViewer.hideAfterTimer.running) {
            /*
             * XXX:
             * コメントビュワーの popup が autohide によって、クリックされると閉じられるため
             * タイマーをチェックし、
             * 非表示にされた直後なら、なにもしない
             */
        } else {
            CommentViewer.toggle();
        }
    },
    loadHandler: function CommentViewer_loadHandler() {
        commentButton.addEventListener('mousedown', CommentViewer.buttonClickHandler, true);
        panelComment.addEventListener('popuphidden', CommentViewer.popupHiddenHandler, false);
        panelComment.addEventListener('popupshown', CommentViewer.popupShownHandler, false);
        // panelComment.addEventListener('mouseout', CommentViewer.popupMouseoutHandler, false);
        listDiv.addEventListener('click', CommentViewer.listClickHandler, true);
        CommentViewer.updateToggle();
    },
    listClickHandler: function CommentViewer_listClickHandler(ev) {
        ev.stopPropagation();
        ev.preventDefault();
        if (ev.target.alt == 'close') {
            CommentViewer.hide();
            return;
        }
        let link = CommentViewer.getHref(ev.target);
        if (link) {
            hOpenUILink(link, ev);
        }
    },
    goEntry: function(ev) {
        let url = CommentViewer.currentURL || CommentViewer.lastRenderData[0];
        hOpenUILink(entryURL(url), ev);
    },
    getHref: function (el) {
        if (typeof el.href != 'undefined') {
            return el.href;
        } else if (el.parentNode) {
            return CommentViewer.getHref(el.parentNode);
        } else {
            return null;
        }
    },
}

EventService.createListener('load', CommentViewer.loadHandler);

<|endoftext|>


<commit_before>d3plus.method.draw = {
  "accepted": [ undefined ],
  "first": true,
  "frozen": false,
  "update": true,
  "value": function ( vars ) {

    if ( !vars.container.value ) {

      var str = vars.format.locale.value.warning.setContainer
      d3plus.console.warning(str)

    }
    else if ( d3.select(vars.container.value).empty() ) {

      var str = vars.format.locale.value.warning.noContainer
      d3plus.console.warning(d3plus.util.format(str,vars.container))

    }
    else {

      d3.select(vars.container.value).call(vars.self)

    }

  }
}
<commit_msg>passing a function to .draw() now enables a callback for each draw<commit_after>d3plus.method.draw = {
  "accepted": [ undefined , Function ],
  "first": true,
  "frozen": false,
  "process": function ( value ) {

    if ( this.initialized === false ) {
      this.initialized = true
      return value
    }

    var vars    = this.getVars()
      , changes = "history" in vars ? vars.history.chain : []

    if ( value === undefined && typeof this.value === "function" ) {
      value = this.value
    }

    if ( vars.container.value === false ) {

      var str = vars.format.locale.value.warning.setContainer
      d3plus.console.warning(str)

    }
    else if ( vars.container.value.empty() ) {
      console.log(vars.container.value.node())
      var str = vars.format.locale.value.warning.noContainer
      d3plus.console.warning(d3plus.util.format(str,vars.container.value))

    }
    else {

      vars.container.value.call(vars.self)

    }

    if ( typeof value === "function" && changes.length ) {

      var changesObject = {}
      changes.forEach(function(c){
        var method = c.method
        delete c.method
        changesObject[method] = c
      })

      value(changesObject)

      vars.history.chain = []

    }

    return value

  },
  "update": true,
  "value": undefined
}
<|endoftext|>


<commit_before>'use strict';

angular.module('clicheApp')
    .controller('HomeCtrl', ['$scope','$q', '$injector', 'Data', 'User', 'Loading', function ($scope, $q, $injector, Data, User, Loading) {

        $scope.view = {};
        $scope.forms = {};

        $scope.view.tab = 'general';
        $scope.view.trace = 'tool';
        $scope.view.showTrace = true;

        $scope.view.tabViewPath = 'views/tabs/general.html';

        $scope.view.propsExpanded = {
            inputs: false,
            outputs: false,
            args: false
        };

        $scope.view.active = {
            inputs: {},
            outputs: {},
            args: {}
        };

        $scope.view.classes = ['page', 'home', 'row'];
        $scope.view.user = null;

        Loading.setClasses($scope.view.classes);

        $scope.Loading = Loading;
        $scope.$watch('Loading.classes', function(n, o) {
            if (n !== o) { $scope.view.classes = n; }
        });

        /* tool form obj */
        $scope.view.toolForm = Data.tool;
        /* job form obj */
        $scope.view.jobForm = Data.job;

        $scope.view.loading = true;

        $scope.$watch('$parent.view.fetch', function(doFetch) {
            if (doFetch) {

                $q.all([
                        Data.fetchTool(),
                        Data.fetchJob(),
                        Data.fetchOwner(),
                        Data.fetchAppId()
                    ]).then(function() {

                        $scope.view.toolForm = Data.tool;
                        $scope.view.toolForm.documentAuthor = null;

                        User.getUser().then(function(result) {
                            $scope.view.user = result.user;
                            if (result.user) {
                                $scope.view.toolForm.documentAuthor = result.user.email;
                                $scope.view.toolForm.softwareDescription.repo_owner = result.user.login;
                                //$scope.view.toolForm.softwareDescription.repo_name = '';
                            }
                        });

                        $scope.view.jobForm = Data.job;

                        $scope.view.owner = Data.owner;
                        $scope.view.appId = Data.appId;

                        $scope.view.loading = false;

                        /* add additional prop attributes */
                        _.each($scope.view.toolForm.inputs.properties, function(prop) {

                            if (_.isUndefined(prop.adapter.separator)) {
                                prop.adapter.separator = '_';
                            }
//                            if (_.isUndefined(prop.adapter.listSeparator)) {
//                                prop.adapter.listSeparator = 'repeat';
//                            }
                        });

                        /* add additional args attributes */
                        _.each($scope.view.toolForm.adapter.args, function(arg) {
                            if (_.isUndefined(arg.separator)) {
                                arg.separator = '_';
                            }
                        });

                    });

            }
        });

        /**
         * Toggle properties visibility (expand/collapse)
         * @param {string} tab
         */
        $scope.toggleProperties = function(tab) {

            $scope.view.propsExpanded[tab] = !$scope.view.propsExpanded[tab];

            var props = (tab !== 'args') ? $scope.view.toolForm[tab].properties : $scope.view.toolForm.adapter.args;

            _.each(props, function(value, key) {
                $scope.view.active[tab][key] = $scope.view.propsExpanded[tab];
            });
        };

        /**
         * Switch the tab
         * @param tab
         */
        $scope.switchTab = function(tab) {
            $scope.view.tab = tab;
            $scope.view.tabViewPath = 'views/tabs/' + tab + '.html';
        };

        /* watchers list */
        var watchers = [];

        /**
         * Turn on deep watch when console tab is visible
         */
        var turnOnDeepWatch = function() {

            Data.generateCommand()
                .then(function (command) {
                    $scope.view.command = command;
                });

            var watch = ['view.jobForm.inputs', 'view.toolForm.inputs.properties', 'view.toolForm.adapter'];

            _.each(watch, function(arg) {
                var watcher = $scope.$watch(arg, function(n, o) {
                    if (n !== o) {
                        Data.generateCommand()
                            .then(function (command) {
                                $scope.view.command = command;
                            });
                    }
                }, true);
                watchers.push(watcher);
            });

        };

        /**
         * Turn off deep watch when console tab is hidden
         */
        var turnOffDeepWatch = function() {

            _.each(watchers, function(watcher) {
                if (_.isFunction(watcher)) {
                    watcher.call();
                }
            });

            watchers = [];
        };

        /**
         * Switch the trace tab
         * @param tab
         */
        $scope.switchTrace = function(tab) {
            $scope.view.trace = tab;

            if (tab === 'console') {
                turnOnDeepWatch();
            } else {
                turnOffDeepWatch();
            }

        };

        /**
         * Toggle trace visibility
         */
        $scope.toggleTrace = function() {
            $scope.view.showTrace = !$scope.view.showTrace;

            if ($scope.view.showTrace) {
                if ($scope.view.trace === 'console') {
                    turnOnDeepWatch();
                }
            } else {
                if ($scope.view.trace === 'console') {
                    turnOffDeepWatch();
                }
            }
        };

        /**
         * Import existing app or app revision
         *
         * @param app
         */
        $scope.importApp = function(appObj) {

            var app = angular.copy(appObj);

            app.json.documentAuthor = $scope.view.user.email;
            app.json.softwareDescription.repo_owner = $scope.view.user.login;
            app.json.softwareDescription.repo_name = '';

            Data.setTool(app.json);
            $scope.view.toolForm = app.json;

            var job = angular.copy($scope.view.jobForm);
            job.inputs = {};

            Data.setJob(job);
            $scope.view.jobForm = job;

            Data.setOwner(app.user_id);
            $scope.view.owner = app.user_id;

            Data.setAppId(app._id);
            $scope.view.appId = app._id;

        };

        /**
         * Store the owner and the app id for the current app
         *
         * @param user_id
         * @param app_id
         */
        $scope.setOwnerAndAppId = function(user_id, app_id) {

            Data.setOwner(user_id);
            $scope.view.owner = user_id;

            Data.setAppId(app_id);
            $scope.view.appId = app_id;

        };

        /**
         * Add item to the baseCmd
         */
        $scope.addBaseCmd = function () {
            $scope.view.toolForm.adapter.baseCmd.push('');
        };

        /**
         * Remove item from the baseCmd
         *
         * @param {integer} index
         * @returns {boolean}
         */
        $scope.removeBaseCmd = function (index) {
            if ($scope.view.toolForm.adapter.baseCmd.length === 1) {
                return false;
            }
            $scope.view.toolForm.adapter.baseCmd.splice(index, 1);
        };

        /**
         * Update baseCmd item if expression defined
         *
         * @param {integer} index
         * @param {*} value
         */
        $scope.updateBaseCmd = function (index, value) {
            $scope.view.toolForm.adapter.baseCmd[index] = value;
        };

        /**
         * Update stdout if expression or literal defined
         *
         * @param {*} value
         */
        $scope.updateStdOut = function (value) {
            $scope.view.toolForm.adapter.stdout = value;
        };

        $scope.updateResource = function (value, key) {

            var SandBox = $injector.get('SandBox');

            $scope.view.toolForm.requirements.resources[key] = value;

            if (_.isObject(value)) {

                SandBox.evaluate(value.expr)
                    .then(function (result) {
                        $scope.view.jobForm.allocatedResources[key] = result;
                    });
            } else {
                if ($scope.view.jobForm.allocatedResources[key] < value) {
                    $scope.view.jobForm.allocatedResources[key] = value;
                }
            }
        };

    }]);
<commit_msg>expression evaluation trigger on resources change<commit_after>'use strict';

angular.module('clicheApp')
    .controller('HomeCtrl', ['$scope','$q', '$injector', 'Data', 'User', 'Loading', 'SandBox', function ($scope, $q, $injector, Data, User, Loading, SandBox) {

        $scope.view = {};
        $scope.forms = {};

        $scope.view.tab = 'general';
        $scope.view.trace = 'tool';
        $scope.view.showTrace = true;

        $scope.view.tabViewPath = 'views/tabs/general.html';

        $scope.view.propsExpanded = {
            inputs: false,
            outputs: false,
            args: false
        };

        $scope.view.active = {
            inputs: {},
            outputs: {},
            args: {}
        };

        $scope.view.classes = ['page', 'home', 'row'];
        $scope.view.user = null;

        Loading.setClasses($scope.view.classes);

        $scope.Loading = Loading;
        $scope.$watch('Loading.classes', function(n, o) {
            if (n !== o) { $scope.view.classes = n; }
        });

        /* tool form obj */
        $scope.view.toolForm = Data.tool;
        /* job form obj */
        $scope.view.jobForm = Data.job;

        $scope.view.loading = true;

        $scope.$watch('$parent.view.fetch', function(doFetch) {
            if (doFetch) {

                $q.all([
                        Data.fetchTool(),
                        Data.fetchJob(),
                        Data.fetchOwner(),
                        Data.fetchAppId()
                    ]).then(function() {

                        $scope.view.toolForm = Data.tool;
                        $scope.view.toolForm.documentAuthor = null;

                        User.getUser().then(function(result) {
                            $scope.view.user = result.user;
                            if (result.user) {
                                $scope.view.toolForm.documentAuthor = result.user.email;
                                $scope.view.toolForm.softwareDescription.repo_owner = result.user.login;
                                //$scope.view.toolForm.softwareDescription.repo_name = '';
                            }
                        });

                        $scope.view.jobForm = Data.job;

                        $scope.view.owner = Data.owner;
                        $scope.view.appId = Data.appId;

                        $scope.view.loading = false;

                        /* add additional prop attributes */
                        _.each($scope.view.toolForm.inputs.properties, function(prop) {

                            if (_.isUndefined(prop.adapter.separator)) {
                                prop.adapter.separator = '_';
                            }
//                            if (_.isUndefined(prop.adapter.listSeparator)) {
//                                prop.adapter.listSeparator = 'repeat';
//                            }
                        });

                        /* add additional args attributes */
                        _.each($scope.view.toolForm.adapter.args, function(arg) {
                            if (_.isUndefined(arg.separator)) {
                                arg.separator = '_';
                            }
                        });

                    });

            }
        });

        /**
         * Toggle properties visibility (expand/collapse)
         * @param {string} tab
         */
        $scope.toggleProperties = function(tab) {

            $scope.view.propsExpanded[tab] = !$scope.view.propsExpanded[tab];

            var props = (tab !== 'args') ? $scope.view.toolForm[tab].properties : $scope.view.toolForm.adapter.args;

            _.each(props, function(value, key) {
                $scope.view.active[tab][key] = $scope.view.propsExpanded[tab];
            });
        };

        /**
         * Switch the tab
         * @param tab
         */
        $scope.switchTab = function(tab) {
            $scope.view.tab = tab;
            $scope.view.tabViewPath = 'views/tabs/' + tab + '.html';

            if (tab === 'values') {
                watchTheJob();
            } else {
                unWatchTheJob();
            }

        };

        /* watchers list */
        var watchers = [];

        /**
         * Turn on deep watch when console tab is visible
         */
        var turnOnDeepWatch = function() {

            Data.generateCommand()
                .then(function (command) {
                    $scope.view.command = command;
                });

            //var watch = ['view.jobForm.inputs', 'view.toolForm.inputs.properties', 'view.toolForm.adapter'];
            var watch = ['view.toolForm.inputs.properties', 'view.toolForm.adapter'];

            _.each(watch, function(arg) {
                var watcher = $scope.$watch(arg, function(n, o) {
                    if (n !== o) {
                        Data.generateCommand()
                            .then(function (command) {
                                $scope.view.command = command;
                            });
                    }
                }, true);
                watchers.push(watcher);
            });

        };

        /**
         * Turn off deep watch when console tab is hidden
         */
        var turnOffDeepWatch = function() {

            _.each(watchers, function(watcher) {
                if (_.isFunction(watcher)) {
                    watcher.call();
                }
            });

            watchers = [];
        };

        var jobWatcher;

        /**
         * Check if there are expressions applied on resources and evaluate them in order to refresh result for the allocated resources
         */
        var checkResources = function () {

            _.each($scope.view.toolForm.requirements.resources, function (resource, key) {

                if (_.isObject(resource) && resource.expr && _.contains(resource.expr, '$job')) {
                    SandBox.evaluate(resource.expr, {})
                        .then(function (result) {
                            $scope.view.jobForm.allocatedResources[key] = result;
                        }, function (error) {
                            console.log(error);
                        });
                }
            });
        };

        /**
         * Watch the job's inputs in order to evaluate expression which include $job as context
         */
        var watchTheJob = function () {

            checkResources();

            if ($scope.view.trace === 'console') {
                Data.generateCommand()
                    .then(function (command) {
                        $scope.view.command = command;
                    });
            }

            jobWatcher = $scope.$watch('view.jobForm.inputs', function(n, o) {
                if (n !== o) {
                    checkResources();
                    if ($scope.view.trace === 'console') {
                        Data.generateCommand()
                            .then(function (command) {
                                $scope.view.command = command;
                            });
                    }
                }
            }, true);

        };

        /**
         * Unwatch job's inputs
         */
        var unWatchTheJob = function () {

            if (_.isFunction(jobWatcher)) {
                jobWatcher.call();
                jobWatcher = undefined;
            }
        };

        /**
         * Switch the trace tab
         * @param tab
         */
        $scope.switchTrace = function(tab) {
            $scope.view.trace = tab;

            if (tab === 'console') {
                turnOnDeepWatch();
            } else {
                turnOffDeepWatch();
            }

        };

        /**
         * Toggle trace visibility
         */
        $scope.toggleTrace = function() {
            $scope.view.showTrace = !$scope.view.showTrace;

            if ($scope.view.showTrace) {
                if ($scope.view.trace === 'console') {
                    turnOnDeepWatch();
                }
            } else {
                if ($scope.view.trace === 'console') {
                    turnOffDeepWatch();
                }
            }
        };

        /**
         * Import existing app or app revision
         *
         * @param app
         */
        $scope.importApp = function(appObj) {

            var app = angular.copy(appObj);

            app.json.documentAuthor = $scope.view.user.email;
            app.json.softwareDescription.repo_owner = $scope.view.user.login;
            app.json.softwareDescription.repo_name = '';

            Data.setTool(app.json);
            $scope.view.toolForm = app.json;

            var job = angular.copy($scope.view.jobForm);
            job.inputs = {};

            Data.setJob(job);
            $scope.view.jobForm = job;

            Data.setOwner(app.user_id);
            $scope.view.owner = app.user_id;

            Data.setAppId(app._id);
            $scope.view.appId = app._id;

        };

        /**
         * Store the owner and the app id for the current app
         *
         * @param user_id
         * @param app_id
         */
        $scope.setOwnerAndAppId = function(user_id, app_id) {

            Data.setOwner(user_id);
            $scope.view.owner = user_id;

            Data.setAppId(app_id);
            $scope.view.appId = app_id;

        };

        /**
         * Add item to the baseCmd
         */
        $scope.addBaseCmd = function () {
            $scope.view.toolForm.adapter.baseCmd.push('');
        };

        /**
         * Remove item from the baseCmd
         *
         * @param {integer} index
         * @returns {boolean}
         */
        $scope.removeBaseCmd = function (index) {
            if ($scope.view.toolForm.adapter.baseCmd.length === 1) {
                return false;
            }
            $scope.view.toolForm.adapter.baseCmd.splice(index, 1);
        };

        /**
         * Update baseCmd item if expression defined
         *
         * @param {integer} index
         * @param {*} value
         */
        $scope.updateBaseCmd = function (index, value) {
            $scope.view.toolForm.adapter.baseCmd[index] = value;
        };

        /**
         * Update stdout if expression or literal defined
         *
         * @param {*} value
         */
        $scope.updateStdOut = function (value) {
            $scope.view.toolForm.adapter.stdout = value;
        };

        /**
         * Update tool resources and apply transformation on allocated resources if needed
         *
         * @param {*} value
         * @param {string} key
         */
        $scope.updateResource = function (value, key) {

            $scope.view.toolForm.requirements.resources[key] = value;

            if (_.isObject(value)) {

                SandBox.evaluate(value.expr, {})
                    .then(function (result) {
                        $scope.view.jobForm.allocatedResources[key] = result;
                    }, function (error) {
                        console.log(error);
                    });
            } else {
                if ($scope.view.jobForm.allocatedResources[key] < value) {
                    $scope.view.jobForm.allocatedResources[key] = value;
                }
            }
        };

    }]);
<|endoftext|>


<commit_before>const { DOWNLOAD_PERIOD, TANK_RADIUS } = require('../../simulation/constants');
const { lerpRotations, getVelocity } = require('../../math/vectorHelpers');

AFRAME.registerComponent('socket-controls', {
  schema: {
    characterId: {default: ''},
    simulationAttribute: {default: 'position'}, // one of 'position', 'tankRotation', 'turretRotation'
    posEnabled: {default: true},
    rotEnabled: {default: true},
    enabled: {default: true}
  },

  init: function() {
    const data = this.data;
    const socket = window.socket;
    if(data.enabled) {
      this.previous = new THREE.Vector3();
      this.current = new THREE.Vector3();
      this.next = new THREE.Vector3();

      this.lastUpdateTime = 0;
      this.updateWaiting = false;
      this.updateRate = DOWNLOAD_PERIOD;
      this.velocity = { x: 0, y: 0, z: 0 };

      if (data.simulationAttribute === 'position') {
        this.controlledAttribute = 'position';
      } else {
        this.controlledAttribute = 'rotation';
      }

      socket.on('simulationUpdate', characters => {
        if (characters[data.characterId]) {
          this.updateWaiting = true;
          this.previous = this.el.getAttribute(this.controlledAttribute);
          this.next = characters[data.characterId][data.simulationAttribute];
        }
      });
    }
  },

  tick: function(t, dt) {
    const data = this.data;
    if(this.updateWaiting) {
      this.updateRate = Math.max(DOWNLOAD_PERIOD, (t - this.lastUpdateTime));
      this.lastUpdateTime = t;
      this.updateWaiting = false;
    }
    const alpha = (t - this.lastUpdateTime) / this.updateRate;

    if (this.controlledAttribute === 'rotation') {
      lerpRotations(this.current, this.previous, this.next, alpha);
    } else {
      this.current.lerpVectors(this.previous, this.next, alpha);
      this.current.y = TANK_RADIUS;
    }

    this.el.setAttribute(this.controlledAttribute, this.current);
  }

});
<commit_msg>Add catch incase not initial position<commit_after>const { DOWNLOAD_PERIOD, TANK_RADIUS } = require('../../simulation/constants');
const { lerpRotations, getVelocity } = require('../../math/vectorHelpers');

AFRAME.registerComponent('socket-controls', {
  schema: {
    characterId: {default: ''},
    simulationAttribute: {default: 'position'}, // one of 'position', 'tankRotation', 'turretRotation'
    posEnabled: {default: true},
    rotEnabled: {default: true},
    enabled: {default: true}
  },

  init: function() {
    const data = this.data;
    const socket = window.socket;
    if(data.enabled) {
      this.previous = new THREE.Vector3();
      this.current = new THREE.Vector3();
      this.next = new THREE.Vector3();

      this.lastUpdateTime = 0;
      this.updateWaiting = false;
      this.updateRate = DOWNLOAD_PERIOD;
      this.velocity = { x: 0, y: 0, z: 0 };

      if (data.simulationAttribute === 'position') {
        this.controlledAttribute = 'position';
      } else {
        this.controlledAttribute = 'rotation';
      }

      socket.on('simulationUpdate', characters => {
        if (characters[data.characterId]) {
          this.updateWaiting = true;
          this.previous = this.el.getAttribute(this.controlledAttribute) || new THREE.Vector3();
          this.next = characters[data.characterId][data.simulationAttribute];
        }
      });
    }
  },

  tick: function(t, dt) {
    const data = this.data;
    if(this.updateWaiting) {
      this.updateRate = Math.max(DOWNLOAD_PERIOD, (t - this.lastUpdateTime));
      this.lastUpdateTime = t;
      this.updateWaiting = false;
    }
    const alpha = (t - this.lastUpdateTime) / this.updateRate;

    if (this.controlledAttribute === 'rotation') {
      lerpRotations(this.current, this.previous, this.next, alpha);
    } else {
      this.current.lerpVectors(this.previous, this.next, alpha);
      this.current.y = TANK_RADIUS;
    }

    this.el.setAttribute(this.controlledAttribute, this.current);
  }

});
<|endoftext|>


<commit_before>'use strict';

angular.module('tdpharmaClientApp')
  .controller('ProductsCtrl', ProductsCtrl);

ProductsCtrl.$inject = ['$cookies', '$filter', '$timeout', 'Category', 'Medicine', 'User', 'toastr', 'S3Upload', 'serverConfig', 'InventoryItem'];

function ProductsCtrl($cookies, $filter, $timeout, Category, Medicine, User, toastr, S3Upload, serverConfig, InventoryItem) {

  var ctrl = this;
  ctrl.tabs = [
    { title:'Category', template:'app/products/includes/category.html', active: true },
    { title:'Details', template:'app/products/includes/details.html', disabled: true }
  ];
  ctrl.categories = [];
  ctrl.amazon_config = {};
  ctrl.medicine = {med_batches_attributes:[{}]};
  ctrl.maxDate = new Date();
  ctrl.selected_user = {};
  ctrl.dateOptions = {
    formatYear: 'yy',
    startingDay: 1
  };
  ctrl.loading = true;
  ctrl.updateUser = updateUser;
  ctrl.open = open;
  ctrl.nextTab = nextTab;
  ctrl.updateTotalAmount = updateTotalAmount;
  ctrl.uploadImage = uploadImage;
  ctrl.saveDirectUpload = saveDirectUpload;
  ctrl.save = save;

  prepareData();

  // Function definition
  function prepareData(){
    User.query().$promise.then(function(resp){
        ctrl.store_users = resp;
    }).catch(function(err){
      toastr.error(error.data.data.errors, $filter('translate')('TOASTR_SORRY'));
    });

    Category.get({}, function(x) {
      ctrl.categories = x.data;
    });

    serverConfig.get({id: 'temp_amazon_s3'}).$promise.then(function(resp){
      ctrl.amazon_config = resp.data;
    }).catch(function(err){
      toastr.error(error.data.data.errors, $filter('translate')('TOASTR_SORRY'));
    })
  }

  function updateUser(item){
    var resp = item;
    _.each(ctrl.medicine.med_batches_attributes, function(b){
      b.user_id = item.id;
    });
  }

  function nextTab() {
    var idx = ctrl.tabs.findIndex(function(x) {return x.active});
    var tab = ctrl.tabs[idx+1];
    tab.active = true;
    tab.disabled = false;
  }

  function updateTotalAmount() {
    if (ctrl.medicine.med_batches_attributes[0].amount_per_pkg && ctrl.medicine.med_batches_attributes[0].amount_unit) {
      ctrl.medicine.med_batches_attributes[0].total_units = ctrl.medicine.med_batches_attributes[0].amount_per_pkg * ctrl.medicine.med_batches_attributes[0].amount_unit;
    }
  }

  function uploadImage(){
    var direct_link;
    if (ctrl.file) {
      var filename = moment().format('YYYYMMDDDDmmssssss');
      direct_link = S3Upload.upload(ctrl.amazon_config, ctrl.file, filename);
    }
    return direct_link;
  }

  function finishSuccess(item){
    ctrl.medicine = {med_batches_attributes:[{user_id: ctrl.selected_user.id}]};
    ctrl.file = null;
    _.extend(ctrl.tabs[0], {active: true, disabled: false});
    _.extend(ctrl.tabs[1], {active: false, disabled: true});
    toastr.success(item.itemable.name + $filter('translate')('TOASTR_CREATED'), $filter('translate')('TOASTR_CONGRATS'));
  }

  function saveDirectUpload(){
    // First attach the category id into the medicine
    if (ctrl.selected[1]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[1].id;
    } else if (ctrl.selected[0]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[0].id;
    }
    // Validate form and save
    if (validateForm()) {
      ctrl.loading = false;
      var params = {
        medicine: ctrl.medicine
      };
      async.waterfall([
          function(callback) {
            // First process the image
            if (ctrl.file) {
              var filename = moment().format('YYYYMMDDDDmmssssss');
              S3Upload.upload(ctrl.amazon_config, ctrl.file, filename).then(function(resp){
                if (resp) {
                  params.direct_upload_url = resp;
                }
                return callback(null, params);
              }).catch(function(error){
                return callback({error: error});
              })
            } else {
              callback(null, params);             // No image to processed so just return the same params
            }
          },
          function(arg1, callback) {
            // Save the medicines
            Medicine.save({}, params).$promise.then(function(resp){
              return callback(null, resp.data);
            }).catch(function(error){
              callback({error: error.data.data.errors});
            });
          },
          function(arg1, callback) {
            // Wait until medicine image is ready
            var item = arg1;
            var retry = 10;
            if (arg1.photo_thumb && arg1.photo_thumb.processed === false) {
              async.whilst(
                function(){
                  return (item.photo_thumb.processed === false);
                },
                function(cb) {
                  toastr.info($filter('translate')('PROCESSING_IMAGE'));
                  InventoryItem.get({id: item.id}).$promise.then(function(resp){
                    item = resp.data;
                  }).catch(function(error){
                    return cb(error.data.data.errors);
                  })
                  retry = retry - 1;
                  if (retry == 0) {
                    return cb($filter('translate')('PROCESSING_IMAGE_TIMEOUT'));
                  }
                  $timeout(cb, 4000)
                },
                function(err) {
                  // Loop completed because of error
                  if (err) {
                    return callback({error: err});
                  } else {
                    // Reset form
                    finishSuccess(item);
                    return callback(null, item);
                  }
                }
              );
            } else {
              // Reset form when there is no photo
              finishSuccess(item);
              return callback(null, item);
            }
          }
        ], function (err, result){
        // After all processes are done or there is an error
        if (err) {
          toastr.error(err.error);
        }
        ctrl.loading = false;
      })
    }
  }

  function save() {
    // First attach the category id into the medicine
    if (ctrl.selected[1]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[1].id;
    } else if (ctrl.selected[0]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[0].id;
    }
    // Validate form and save
    if (validateForm()) {
      var params = {
        medicine: ctrl.medicine
      };
      if (ctrl.file) {
        params.image = ctrl.file.$ngfDataUrl;
      }
      Medicine.save({}, params).$promise.then(function(resp){
        // Reset form
        ctrl.medicine = {med_batches_attributes:[{user_id: ctrl.selected_user.id}]};
        ctrl.file = null;
        _.extend(ctrl.tabs[0], {active: true, disabled: false});
        _.extend(ctrl.tabs[1], {active: false, disabled: true});
        toastr.success(resp.data.name + $filter('translate')('TOASTR_CREATED'), $filter('translate')('TOASTR_CONGRATS'))
      }).catch(function(error){
        toastr.error(error.data.data.errors, $filter('translate')('TOASTR_SORRY'));
      });
    }
  }

  function validateForm(){
    var ok_flag = true;
    if (!ctrl.medicine.med_batches_attributes[0].user_id) {
      toastr.error($filter('translate')('AUTHOR_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].category_id) {
      toastr.error($filter('translate')('CATEGORY_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.name) {
      toastr.error($filter('translate')('NAME_REQUIRED'));
      ok_flag = false;
    }
    if ( ok_flag && !ctrl.medicine.concentration) {
     toastr.error($filter('translate')('CONCENTRATION_REQUIRED'));
     ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.concentration_unit) {
      toastr.error($filter('translate')('CONCENTRATION_UNIT_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.mfg_location) {
      toastr.error($filter('translate')('MFG_LOCATION_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.manufacturer) {
      toastr.error($filter('translate')('MANUFACTURER_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].mfg_date){
      toastr.error($filter('translate')('MFG_DATE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].expire_date) {
      toastr.error($filter('translate')('EXPIRE_DATE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].package){
      toastr.error($filter('translate')('PACKAGE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].amount_per_pkg) {
      toastr.error($filter('translate')('AMOUNT_PER_PKG_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].amount_unit) {
      toastr.error($filter('translate')('AMOUNT_UNIT_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].total_units) {
      toastr.error($filter('translate')('TOTAL_UNITS_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].total_price) {
      toastr.error($filter('translate')('PRICE_REQUIRED'));
      ok_flag = false;
    }
    return ok_flag;
  }

  function open(status, $event, $index) {
    ctrl[status] = ctrl[status] || [];
    ctrl[status][$index] = ctrl[status][$index] || {};
    ctrl[status][$index].opened = true;
  };

}
<commit_msg>Fixed displaying error messages on add products view<commit_after>'use strict';

angular.module('tdpharmaClientApp')
  .controller('ProductsCtrl', ProductsCtrl);

ProductsCtrl.$inject = ['$cookies', '$filter', '$timeout', 'Category', 'Medicine', 'User', 'toastr', 'S3Upload', 'serverConfig', 'InventoryItem'];

function ProductsCtrl($cookies, $filter, $timeout, Category, Medicine, User, toastr, S3Upload, serverConfig, InventoryItem) {

  var ctrl = this;
  ctrl.tabs = [
    { title:'Category', template:'app/products/includes/category.html', active: true },
    { title:'Details', template:'app/products/includes/details.html', disabled: true }
  ];
  ctrl.categories = [];
  ctrl.amazon_config = {};
  ctrl.medicine = {med_batches_attributes:[{}]};
  ctrl.maxDate = new Date();
  ctrl.selected_user = {};
  ctrl.dateOptions = {
    formatYear: 'yy',
    startingDay: 1
  };
  ctrl.loading = true;
  ctrl.updateUser = updateUser;
  ctrl.open = open;
  ctrl.nextTab = nextTab;
  ctrl.updateTotalAmount = updateTotalAmount;
  ctrl.uploadImage = uploadImage;
  ctrl.saveDirectUpload = saveDirectUpload;
  ctrl.save = save;

  prepareData();

  // Function definition
  function prepareData(){
    User.query().$promise.then(function(resp){
        ctrl.store_users = resp;
    }).catch(function(err){
      toastr.error(err.data.data.errors, $filter('translate')('TOASTR_SORRY'));
    });

    Category.get({}, function(x) {
      ctrl.categories = x.data;
    });

    serverConfig.get({id: 'temp_amazon_s3'}).$promise.then(function(resp){
      ctrl.amazon_config = resp.data;
    }).catch(function(err){
      toastr.error(err.data.data.errors, $filter('translate')('TOASTR_SORRY'));
    })
  }

  function updateUser(item){
    var resp = item;
    _.each(ctrl.medicine.med_batches_attributes, function(b){
      b.user_id = item.id;
    });
  }

  function nextTab() {
    var idx = ctrl.tabs.findIndex(function(x) {return x.active});
    var tab = ctrl.tabs[idx+1];
    tab.active = true;
    tab.disabled = false;
  }

  function updateTotalAmount() {
    if (ctrl.medicine.med_batches_attributes[0].amount_per_pkg && ctrl.medicine.med_batches_attributes[0].amount_unit) {
      ctrl.medicine.med_batches_attributes[0].total_units = ctrl.medicine.med_batches_attributes[0].amount_per_pkg * ctrl.medicine.med_batches_attributes[0].amount_unit;
    }
  }

  function uploadImage(){
    var direct_link;
    if (ctrl.file) {
      var filename = moment().format('YYYYMMDDDDmmssssss');
      direct_link = S3Upload.upload(ctrl.amazon_config, ctrl.file, filename);
    }
    return direct_link;
  }

  function finishSuccess(item){
    ctrl.medicine = {med_batches_attributes:[{user_id: ctrl.selected_user.id}]};
    ctrl.file = null;
    _.extend(ctrl.tabs[0], {active: true, disabled: false});
    _.extend(ctrl.tabs[1], {active: false, disabled: true});
    toastr.success(item.itemable.name + $filter('translate')('TOASTR_CREATED'), $filter('translate')('TOASTR_CONGRATS'));
  }

  function saveDirectUpload(){
    // First attach the category id into the medicine
    if (ctrl.selected[1]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[1].id;
    } else if (ctrl.selected[0]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[0].id;
    }
    // Validate form and save
    if (validateForm()) {
      ctrl.loading = false;
      var params = {
        medicine: ctrl.medicine
      };
      async.waterfall([
          function(callback) {
            // First process the image
            if (ctrl.file) {
              var filename = moment().format('YYYYMMDDDDmmssssss');
              S3Upload.upload(ctrl.amazon_config, ctrl.file, filename).then(function(resp){
                if (resp) {
                  params.direct_upload_url = resp;
                }
                return callback(null, params);
              }).catch(function(error){
                return callback({error: error});
              })
            } else {
              callback(null, params);             // No image to processed so just return the same params
            }
          },
          function(arg1, callback) {
            // Save the medicines
            Medicine.save({}, params).$promise.then(function(resp){
              return callback(null, resp.data);
            }).catch(function(error){
              callback({error: error.data.data.errors});
            });
          },
          function(arg1, callback) {
            // Wait until medicine image is ready
            var item = arg1;
            var retry = 10;
            if (arg1.photo_thumb && arg1.photo_thumb.processed === false) {
              async.whilst(
                function(){
                  return (item.photo_thumb.processed === false);
                },
                function(cb) {
                  toastr.info($filter('translate')('PROCESSING_IMAGE'));
                  InventoryItem.get({id: item.id}).$promise.then(function(resp){
                    item = resp.data;
                  }).catch(function(error){
                    return cb(error.data.data.errors);
                  })
                  retry = retry - 1;
                  if (retry == 0) {
                    return cb($filter('translate')('PROCESSING_IMAGE_TIMEOUT'));
                  }
                  $timeout(cb, 4000)
                },
                function(err) {
                  // Loop completed because of error
                  if (err) {
                    return callback({error: err});
                  } else {
                    // Reset form
                    finishSuccess(item);
                    return callback(null, item);
                  }
                }
              );
            } else {
              // Reset form when there is no photo
              finishSuccess(item);
              return callback(null, item);
            }
          }
        ], function (err, result){
        // After all processes are done or there is an error
        if (err) {
          toastr.error(err.error);
        }
        ctrl.loading = false;
      })
    }
  }

  function save() {
    // First attach the category id into the medicine
    if (ctrl.selected[1]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[1].id;
    } else if (ctrl.selected[0]) {
      ctrl.medicine.med_batches_attributes[0].category_id = ctrl.selected[0].id;
    }
    // Validate form and save
    if (validateForm()) {
      var params = {
        medicine: ctrl.medicine
      };
      if (ctrl.file) {
        params.image = ctrl.file.$ngfDataUrl;
      }
      Medicine.save({}, params).$promise.then(function(resp){
        // Reset form
        ctrl.medicine = {med_batches_attributes:[{user_id: ctrl.selected_user.id}]};
        ctrl.file = null;
        _.extend(ctrl.tabs[0], {active: true, disabled: false});
        _.extend(ctrl.tabs[1], {active: false, disabled: true});
        toastr.success(resp.data.name + $filter('translate')('TOASTR_CREATED'), $filter('translate')('TOASTR_CONGRATS'))
      }).catch(function(error){
        toastr.error(error.data.data.errors, $filter('translate')('TOASTR_SORRY'));
      });
    }
  }

  function validateForm(){
    var ok_flag = true;
    if (!ctrl.medicine.med_batches_attributes[0].user_id) {
      toastr.error($filter('translate')('AUTHOR_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].category_id) {
      toastr.error($filter('translate')('CATEGORY_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.name) {
      toastr.error($filter('translate')('NAME_REQUIRED'));
      ok_flag = false;
    }
    if ( ok_flag && !ctrl.medicine.concentration) {
     toastr.error($filter('translate')('CONCENTRATION_REQUIRED'));
     ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.concentration_unit) {
      toastr.error($filter('translate')('CONCENTRATION_UNIT_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.mfg_location) {
      toastr.error($filter('translate')('MFG_LOCATION_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.manufacturer) {
      toastr.error($filter('translate')('MANUFACTURER_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].mfg_date){
      toastr.error($filter('translate')('MFG_DATE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].expire_date) {
      toastr.error($filter('translate')('EXPIRE_DATE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].package){
      toastr.error($filter('translate')('PACKAGE_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].amount_per_pkg) {
      toastr.error($filter('translate')('AMOUNT_PER_PKG_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].amount_unit) {
      toastr.error($filter('translate')('AMOUNT_UNIT_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].total_units) {
      toastr.error($filter('translate')('TOTAL_UNITS_REQUIRED'));
      ok_flag = false;
    }
    if (ok_flag && !ctrl.medicine.med_batches_attributes[0].total_price) {
      toastr.error($filter('translate')('PRICE_REQUIRED'));
      ok_flag = false;
    }
    return ok_flag;
  }

  function open(status, $event, $index) {
    ctrl[status] = ctrl[status] || [];
    ctrl[status][$index] = ctrl[status][$index] || {};
    ctrl[status][$index].opened = true;
  };

}
<|endoftext|>