<commit_before>'use strict';
function Popup () {

}

//creates 7:00 - 9:00 am
// Thursday, Friday string
var weekDays = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saterday']
Popup.prototype.createTimeStrings = function(meetings) {

	//{startDate: 16554, endDate: 16554, profs: Array[1], where: "Snell Engineering Center 168", times: Object…}
	meetings.forEach(function (meeting) {

		var timeText = []
		// "[{"times":[{"start":46800,"end":54000}],"days":["3"]}]"
		meeting.groupedTimes.forEach(function (groupedTime) {
			groupedTime.times.forEach(function (time,index) {
				if (index>0) {
					timeText.push(', ')
				};

				timeText.push(moment.utc(time.start*1000).format('h:mm'))

				timeText.push(' - ')

				timeText.push(moment.utc(time.end*1000).format('h:mm a'))
			}.bind(this))

		}.bind(this))
		meeting.timeString = timeText.join('')
		meeting.dayString = ''

		for (var dayIndex in meeting.times) {
			if (!weekDays[dayIndex]) {
				console.log('error dayIndex not found?')
				meeting.dayString+='Someday'
			}
			else {
				meeting.dayString+=weekDays[dayIndex]+', '
			}
		}
		meeting.dayString = meeting.dayString.trim().replace(/,$/gi,'')
	}.bind(this))
}
Popup.prototype.calculateHoursPerWeek = function(meetings) {
	meetings.forEach(function (meeting) {
		meeting.hoursPerWeek = 0;

		for (var dayIndex in meeting.times) {
			var dayTimes = meeting.times[dayIndex]
			dayTimes.forEach(function (time) {
				//end and start are in seconds so conver them to hours
				meeting.hoursPerWeek += (time.end - time.start)/(60*60)
			}.bind(this))
		}

		meeting.hoursPerWeek = Math.round(10*meeting.hoursPerWeek)/10
	}.bind(this))
}

Popup.prototype.addTimestoGroupedTimes = function(meeting,dayIndex) {
	var times = meeting.times[dayIndex]

	for (var i = 0; i < meeting.groupedTimes.length; i++) {
		if (_.isEqual(meeting.groupedTimes[i].times,times)){
			meeting.groupedTimes[i].days.push(dayIndex)
			return;
		}
	}
	meeting.groupedTimes.push({
		times:times,
		days:[dayIndex]
	})
}

Popup.prototype.calculateExams = function(meetings) {
	meetings.forEach(function (meeting) {
		if (meeting.startDate==meeting.endDate) {
			meeting.isExam = true;
		}
		else {
			meeting.isExam = false;
		}

	}.bind(this))
};
Popup.prototype.prettyLocation = function(location) {
	if (location.toLowerCase()==='tba' || location==='') {
		return 'location undecided'
	}
	else if (location.toLowerCase()=='web online') {
		return 'Web online'

	}
	else {
		return ''+location+'';
	}
};

Popup.prototype.groupSectionTimes = function(sections) {
	//make a list of all profs
	sections.forEach(function (section) {
		if (!section.meetings) {
			return;
		}
		section.profs = []
		section.locations = []

		section.meetings.forEach(function (meeting) {

			//make a big list of all meetings prof's in the section
			meeting.profs.forEach(function (prof) {
				if (section.profs.indexOf(prof)<0) {
					section.profs.push(prof);
				};
			}.bind(this))

			if (section.locations.indexOf(meeting.where)<0) {
				section.locations.push(this.prettyLocation(meeting.where));
			};

		}.bind(this))

		//group the times by start/end time (so can put days underneath)
		// var meetingsGrouped = []
		section.meetings.forEach(function (meeting) {
			meeting.groupedTimes = [];
			for (var dayIndex in meeting.times) {
				this.addTimestoGroupedTimes(meeting,dayIndex)
			}
		}.bind(this))

		this.createTimeStrings(section.meetings)
		this.calculateHoursPerWeek(section.meetings);
		this.calculateExams(section.meetings);

		section.profs = section.profs.join(', ');
		section.locations = section.locations.join(', ');
	}.bind(this))

	//sort sections by first grouped time start time
	sections.sort(function(a,b){
	  if (!a.meetings && !b.meetings) {
	    return 0;
	  }
	  if (!a.meetings || a.meetings.length===0) {
	    return 1;
	  }
	  if (!b.meetings || b.meetings.length===0) {
	    return -1;
	  }

	  if (a.meetings[0].groupedTimes.length===0) {
	    return 1;
	  }
	  if (b.meetings[0].groupedTimes.length===0) {
	    return -1;
	  }
	  if (a.meetings[0].groupedTimes[0].times.length===0) {
	    return 1;
	  }
	  if (b.meetings[0].groupedTimes[0].times.length===0) {
	    return -1;
	  }
	  if (a.meetings[0].groupedTimes[0].times[0].start>b.meetings[0].groupedTimes[0].times[0].start) {
	    return 1;
	  }
	  else if (a.meetings[0].groupedTimes[0].times[0].start<b.meetings[0].groupedTimes[0].times[0].start) {
	    return -1;
	  }
	  else {
	    return 0;
	  }

	}.bind(this))
	//

	return sections;
}
Popup.prototype.createViewOnUrl = function(tree,url) {
	return 'View on '+tree.host+''
}
Popup.prototype.createCreditsHTML = function(tree) {
	if (tree.minCredits!==undefined || tree.maxCredits!==undefined) {
		if (tree.minCredits===tree.maxCredits) {
			return tree.minCredits+' credit'+render.getOptionalS(tree.minCredits);
		}
		else  {
			return tree.minCredits+' to '+tree.maxCredits+' credits';
		}
	}
	return ''
}
Popup.prototype.removeSectionsNotInClass = function(tree,sections) {
	if (!tree.crns) {
		console.log('error class has no crns!!!')
		return sections
	};

	var retVal = [];
	sections.forEach(function (section) {
		if (_(tree.crns).includes(section.crn)) {
			retVal.push(section)
		};
	}.bind(this))
	return retVal;
}

Popup.prototype.expandPanel = function(tree) {
	if (tree.isString) {
		return;
	};
	if (tree.dataStatus!==treeMgr.DATASTATUS_DONE) {
		return;
	};

	var xButton = tree.panel.getElementsByClassName('glyphicon-remove')[0]
	xButton.style.display = ''

	tree.panel.style.zIndex = '999'
	tree.isExpanded=true;
	var panelBody = tree.panel.getElementsByClassName('panelBodyId')[0]

	//overall
	//credits
	//nofity if changes
	//update button

	//if not much details don't expand
	if ((!tree.desc || tree.desc.length<20) && (!tree.crns || tree.crns.length===0)) {
		panelBody.style.whiteSpace ='normal'

		panelBody.innerHTML+=this.createCreditsHTML(tree)

		var classURL ;
		if (tree.prettyUrl) {
			classURL = tree.prettyUrl;
		}
		else {
			classURL = tree.url;
		}
		panelBody.innerHTML += '
 view on '+tree.host+''
	}
	else {
		request({
			url:'/listSections',
			type:'POST',
			body:JSON.stringify({
				host:tree.host,
				termId:tree.termId,
				subject:tree.subject,
				classId:tree.classId
			})
		},function (err,body) {
			if (err) {
				console.log(err);
				return;
			}

			body = this.removeSectionsNotInClass(tree,body)

			//tree was minimized before server responded...
			if (!tree.isExpanded) {
				return;
			}

			var newBodyText = []
			var panelWidth = 0;

			//description box
			if (tree.desc) {
				newBodyText.push('
')
				newBodyText.push(tree.desc)
				newBodyText.push('
')

				// if (tree.desc.length>100) {
				// 	panelWidth = tree.desc.length
				// };
			};

			//credits and class url
			newBodyText.push('
')

			//add credits to the left
			if (tree.minCredits!==undefined || tree.maxCredits!==undefined) {
				newBodyText.push('
'+this.createCreditsHTML(tree)+'
')
			}

			var classURL ;
			if (tree.prettyUrl) {
				classURL = tree.prettyUrl;
			}
			else {
				classURL = tree.url;
			}

			newBodyText.push('
'+this.createViewOnUrl(tree,classURL)+'
')

			newBodyText.push('
')
			newBodyText.push('
')

			var sections = this.groupSectionTimes(body);
			sections.forEach(function (section) {

				if (section.meetings) {
					panelWidth += 330
					newBodyText.push('
')

					section.meetings.forEach(function (meeting) {
						if (meeting.hoursPerWeek===0) {
							return;
						};

						if (meeting.isExam) {
							newBodyText.push( '
Exam
'+meeting.timeString+' '+moment((meeting.startDate+1)*24*60*60*1000).format('dddd MMM Do')+'
'+meeting.hoursPerWeek+' hours
 '+this.prettyLocation(meeting.where)+'
')
						}
						else {
							newBodyText.push( '
'+meeting.timeString+'
 '+meeting.dayString+'
'+meeting.hoursPerWeek+' hours/week
 '+this.prettyLocation(meeting.where)+'
')
						}
					}.bind(this))
				}
				else {
					panelWidth += 185
					newBodyText.push('
')
				}

				newBodyText.push( '
')

				var leftBoxText;

				if (section.meetings) {
					leftBoxText = section.profs+'
'+section.crn
				}
				else {
					leftBoxText = section.crn
				}
				newBodyText.push( '
'+leftBoxText+'
')

				newBodyText.push( '
 '+section.seatsRemaining+'/'+section.seatsCapacity+' seats
'+this.createViewOnUrl(tree,section.url)+'
')

				newBodyText.push('
')
				newBodyText.push('
')

			}.bind(this))
			//
			newBodyText.push('
')

			panelBody.style.whiteSpace = 'initial'
			panelBody.innerHTML=newBodyText.join('');

			tree.panel.getElementsByClassName('rightSectionText')

			var elements = [].slice.call(tree.panel.getElementsByClassName('rightSectionText'))

			elements.forEach(function (element) {
				element.style.minWidth = element.getElementsByClassName('hostName')[0].offsetWidth + 'px'
			}.bind(this))
			//

			panelBody.style.marginBottom = "-10px"

			tree.panel.style.height = ''

			if (!panelWidth) {
				if (tree.desc) {
					panelWidth = tree.desc.length
				}
				else {
					panelWidth = ''
				}
			}
			else {
				panelWidth = Math.min(1000,panelWidth)
			}

			//width + left offset
			tree.panel.style.width = panelWidth+ 'px'
			tree.panel.style.maxWidth = '890px'

			var left = tree.x - tree.panel.offsetWidth/2
			if (left<15) {
				left = 15
			};

			tree.panel.style.left = left + 'px'

			//height + top offset
			tree.panel.style.top = Math.max(tree.y - tree.panel.offsetHeight/2,$('.navbar')[0].offsetHeight+25 ) + 'px'

			//shadows are cool
			tree.panel.style.boxShadow = 'gray 0px 0px 9px'

		}.bind(this))

		//per section
		//hours per week
		//crn
		//link
		//prof
		//location - gps coords or href this somehow?
		//meetings times
		//total seats, seats open
		//nofity if seat opens
		//update button

		//all the data in meetings
		//any other data
	}
}

Popup.prototype.addPopups = function(tree) {

	if (tree.isClass) {

		tree.panel.onclick=function (event) {

			ga('send', 'event', 'category', 'action', {'type':'popup','name': tree.name,'classId':tree.classId,'subject':tree.subject});

			if (tree.isExpanded) {
				render.resetPanel(tree);
			}
			else {
				this.expandPanel(tree);
			}
		}.bind(this)
	}

	if (tree.values) {
		tree.values.forEach(function (subTree) {
			this.addPopups(subTree);
		}.bind(this));
	};
}

Popup.prototype.go = function(tree) {
	this.tree = tree;
};

Popup.prototype.Popup=Popup;
window.popup = new Popup();
<commit_msg>show x after server responds<commit_after>'use strict';
function Popup () {

}

//creates 7:00 - 9:00 am
// Thursday, Friday string
var weekDays = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saterday']
Popup.prototype.createTimeStrings = function(meetings) {

	//{startDate: 16554, endDate: 16554, profs: Array[1], where: "Snell Engineering Center 168", times: Object…}
	meetings.forEach(function (meeting) {

		var timeText = []
		// "[{"times":[{"start":46800,"end":54000}],"days":["3"]}]"
		meeting.groupedTimes.forEach(function (groupedTime) {
			groupedTime.times.forEach(function (time,index) {
				if (index>0) {
					timeText.push(', ')
				};

				timeText.push(moment.utc(time.start*1000).format('h:mm'))

				timeText.push(' - ')

				timeText.push(moment.utc(time.end*1000).format('h:mm a'))
			}.bind(this))

		}.bind(this))
		meeting.timeString = timeText.join('')
		meeting.dayString = ''

		for (var dayIndex in meeting.times) {
			if (!weekDays[dayIndex]) {
				console.log('error dayIndex not found?')
				meeting.dayString+='Someday'
			}
			else {
				meeting.dayString+=weekDays[dayIndex]+', '
			}
		}
		meeting.dayString = meeting.dayString.trim().replace(/,$/gi,'')
	}.bind(this))
}
Popup.prototype.calculateHoursPerWeek = function(meetings) {
	meetings.forEach(function (meeting) {
		meeting.hoursPerWeek = 0;

		for (var dayIndex in meeting.times) {
			var dayTimes = meeting.times[dayIndex]
			dayTimes.forEach(function (time) {
				//end and start are in seconds so conver them to hours
				meeting.hoursPerWeek += (time.end - time.start)/(60*60)
			}.bind(this))
		}

		meeting.hoursPerWeek = Math.round(10*meeting.hoursPerWeek)/10
	}.bind(this))
}

Popup.prototype.addTimestoGroupedTimes = function(meeting,dayIndex) {
	var times = meeting.times[dayIndex]

	for (var i = 0; i < meeting.groupedTimes.length; i++) {
		if (_.isEqual(meeting.groupedTimes[i].times,times)){
			meeting.groupedTimes[i].days.push(dayIndex)
			return;
		}
	}
	meeting.groupedTimes.push({
		times:times,
		days:[dayIndex]
	})
}

Popup.prototype.calculateExams = function(meetings) {
	meetings.forEach(function (meeting) {
		if (meeting.startDate==meeting.endDate) {
			meeting.isExam = true;
		}
		else {
			meeting.isExam = false;
		}

	}.bind(this))
};
Popup.prototype.prettyLocation = function(location) {
	if (location.toLowerCase()==='tba' || location==='') {
		return 'location undecided'
	}
	else if (location.toLowerCase()=='web online') {
		return 'Web online'

	}
	else {
		return ''+location+'';
	}
};

Popup.prototype.groupSectionTimes = function(sections) {
	//make a list of all profs
	sections.forEach(function (section) {
		if (!section.meetings) {
			return;
		}
		section.profs = []
		section.locations = []

		section.meetings.forEach(function (meeting) {

			//make a big list of all meetings prof's in the section
			meeting.profs.forEach(function (prof) {
				if (section.profs.indexOf(prof)<0) {
					section.profs.push(prof);
				};
			}.bind(this))

			if (section.locations.indexOf(meeting.where)<0) {
				section.locations.push(this.prettyLocation(meeting.where));
			};

		}.bind(this))

		//group the times by start/end time (so can put days underneath)
		// var meetingsGrouped = []
		section.meetings.forEach(function (meeting) {
			meeting.groupedTimes = [];
			for (var dayIndex in meeting.times) {
				this.addTimestoGroupedTimes(meeting,dayIndex)
			}
		}.bind(this))

		this.createTimeStrings(section.meetings)
		this.calculateHoursPerWeek(section.meetings);
		this.calculateExams(section.meetings);

		section.profs = section.profs.join(', ');
		section.locations = section.locations.join(', ');
	}.bind(this))

	//sort sections by first grouped time start time
	sections.sort(function(a,b){
	  if (!a.meetings && !b.meetings) {
	    return 0;
	  }
	  if (!a.meetings || a.meetings.length===0) {
	    return 1;
	  }
	  if (!b.meetings || b.meetings.length===0) {
	    return -1;
	  }

	  if (a.meetings[0].groupedTimes.length===0) {
	    return 1;
	  }
	  if (b.meetings[0].groupedTimes.length===0) {
	    return -1;
	  }
	  if (a.meetings[0].groupedTimes[0].times.length===0) {
	    return 1;
	  }
	  if (b.meetings[0].groupedTimes[0].times.length===0) {
	    return -1;
	  }
	  if (a.meetings[0].groupedTimes[0].times[0].start>b.meetings[0].groupedTimes[0].times[0].start) {
	    return 1;
	  }
	  else if (a.meetings[0].groupedTimes[0].times[0].start<b.meetings[0].groupedTimes[0].times[0].start) {
	    return -1;
	  }
	  else {
	    return 0;
	  }

	}.bind(this))
	//

	return sections;
}
Popup.prototype.createViewOnUrl = function(tree,url) {
	return 'View on '+tree.host+''
}
Popup.prototype.createCreditsHTML = function(tree) {
	if (tree.minCredits!==undefined || tree.maxCredits!==undefined) {
		if (tree.minCredits===tree.maxCredits) {
			return tree.minCredits+' credit'+render.getOptionalS(tree.minCredits);
		}
		else  {
			return tree.minCredits+' to '+tree.maxCredits+' credits';
		}
	}
	return ''
}
Popup.prototype.removeSectionsNotInClass = function(tree,sections) {
	if (!tree.crns) {
		console.log('error class has no crns!!!')
		return sections
	};

	var retVal = [];
	sections.forEach(function (section) {
		if (_(tree.crns).includes(section.crn)) {
			retVal.push(section)
		};
	}.bind(this))
	return retVal;
}

Popup.prototype.expandPanel = function(tree) {
	if (tree.isString) {
		return;
	};
	if (tree.dataStatus!==treeMgr.DATASTATUS_DONE) {
		return;
	};

	tree.panel.style.zIndex = '999'
	tree.isExpanded=true;
	var panelBody = tree.panel.getElementsByClassName('panelBodyId')[0]

	//overall
	//credits
	//nofity if changes
	//update button

	//if not much details don't expand
	if ((!tree.desc || tree.desc.length<20) && (!tree.crns || tree.crns.length===0)) {
		panelBody.style.whiteSpace ='normal'

		panelBody.innerHTML+=this.createCreditsHTML(tree)

		var classURL ;
		if (tree.prettyUrl) {
			classURL = tree.prettyUrl;
		}
		else {
			classURL = tree.url;
		}
		panelBody.innerHTML += '
 view on '+tree.host+''
	}
	else {
		request({
			url:'/listSections',
			type:'POST',
			body:JSON.stringify({
				host:tree.host,
				termId:tree.termId,
				subject:tree.subject,
				classId:tree.classId
			})
		},function (err,body) {
			if (err) {
				console.log(err);
				return;
			}

			body = this.removeSectionsNotInClass(tree,body)

			//tree was minimized before server responded...
			if (!tree.isExpanded) {
				return;
			}

			var newBodyText = []
			var panelWidth = 0;

			//description box
			if (tree.desc) {
				newBodyText.push('
')
				newBodyText.push(tree.desc)
				newBodyText.push('
')
			};

			//credits and class url
			newBodyText.push('
')

			//add credits to the left
			if (tree.minCredits!==undefined || tree.maxCredits!==undefined) {
				newBodyText.push('
'+this.createCreditsHTML(tree)+'
')
			}

			var classURL ;
			if (tree.prettyUrl) {
				classURL = tree.prettyUrl;
			}
			else {
				classURL = tree.url;
			}

			newBodyText.push('
'+this.createViewOnUrl(tree,classURL)+'
')

			newBodyText.push('
')
			newBodyText.push('
')

			var sections = this.groupSectionTimes(body);
			sections.forEach(function (section) {

				if (section.meetings) {
					panelWidth += 330
					newBodyText.push('
')

					section.meetings.forEach(function (meeting) {
						if (meeting.hoursPerWeek===0) {
							return;
						};

						if (meeting.isExam) {
							newBodyText.push( '
Exam
'+meeting.timeString+' '+moment((meeting.startDate+1)*24*60*60*1000).format('dddd MMM Do')+'
'+meeting.hoursPerWeek+' hours
 '+this.prettyLocation(meeting.where)+'
')
						}
						else {
							newBodyText.push( '
'+meeting.timeString+'
 '+meeting.dayString+'
'+meeting.hoursPerWeek+' hours/week
 '+this.prettyLocation(meeting.where)+'
')
						}
					}.bind(this))
				}
				else {
					panelWidth += 185
					newBodyText.push('
')
				}

				newBodyText.push( '
')

				var leftBoxText;

				if (section.meetings) {
					leftBoxText = section.profs+'
'+section.crn
				}
				else {
					leftBoxText = section.crn
				}
				newBodyText.push( '
'+leftBoxText+'
')

				newBodyText.push( '
 '+section.seatsRemaining+'/'+section.seatsCapacity+' seats
'+this.createViewOnUrl(tree,section.url)+'
')

				newBodyText.push('
')
				newBodyText.push('
')

			}.bind(this))
			//
			newBodyText.push('
')

			panelBody.style.whiteSpace = 'initial'
			panelBody.innerHTML=newBodyText.join('');

			tree.panel.getElementsByClassName('rightSectionText')

			var elements = [].slice.call(tree.panel.getElementsByClassName('rightSectionText'))

			elements.forEach(function (element) {
				element.style.minWidth = element.getElementsByClassName('hostName')[0].offsetWidth + 'px'
			}.bind(this))
			//

			panelBody.style.marginBottom = "-10px"

			tree.panel.style.height = ''

			if (!panelWidth) {
				if (tree.desc) {
					panelWidth = tree.desc.length
				}
				else {
					panelWidth = ''
				}
			}
			else {
				panelWidth = Math.min(1000,panelWidth)
			}

			//width + left offset
			tree.panel.style.width = panelWidth+ 'px'
			tree.panel.style.maxWidth = '890px'

			var left = tree.x - tree.panel.offsetWidth/2
			if (left<15) {
				left = 15
			};

			tree.panel.style.left = left + 'px'

			//height + top offset
			tree.panel.style.top = Math.max(tree.y - tree.panel.offsetHeight/2,$('.navbar')[0].offsetHeight+25 ) + 'px'

			//shadows are cool
			tree.panel.style.boxShadow = 'gray 0px 0px 9px'

			//show the x button
			var xButton = tree.panel.getElementsByClassName('glyphicon-remove')[0]
			xButton.style.display = ''

		}.bind(this))

		//per section
		//hours per week
		//crn
		//link
		//prof
		//location - gps coords or href this somehow?
		//meetings times
		//total seats, seats open
		//nofity if seat opens
		//update button

		//all the data in meetings
		//any other data
	}
}

Popup.prototype.addPopups = function(tree) {

	if (tree.isClass) {

		tree.panel.onclick=function (event) {

			ga('send', 'event', 'category', 'action', {'type':'popup','name': tree.name,'classId':tree.classId,'subject':tree.subject});

			if (tree.isExpanded) {
				render.resetPanel(tree);
			}
			else {
				this.expandPanel(tree);
			}
		}.bind(this)
	}

	if (tree.values) {
		tree.values.forEach(function (subTree) {
			this.addPopups(subTree);
		}.bind(this));
	};
}

Popup.prototype.go = function(tree) {
	this.tree = tree;
};

Popup.prototype.Popup=Popup;
window.popup = new Popup();
<|endoftext|>


<commit_before>// The Tag UI area associated with a post

/*global window, document, setTimeout, $, _, Backbone, Ghost */

(function () {
    "use strict";

    Ghost.View.EditorTagWidget = Ghost.View.extend({

        events: {
            'keyup [data-input-behaviour="tag"]': 'handleKeyup',
            'keydown [data-input-behaviour="tag"]': 'handleKeydown',
            'click ul.suggestions li': 'handleSuggestionClick',
            'click .tags .tag': 'handleTagClick',
            'click .tag-label': 'mobileTags'
        },

        keys: {
            UP: 38,
            DOWN: 40,
            ESC: 27,
            ENTER: 13,
            COMMA: 188,
            BACKSPACE: 8
        },

        initialize: function () {
            var self = this,
                tagCollection = new Ghost.Collections.Tags();

            tagCollection.fetch().then(function () {
                self.allGhostTags = tagCollection.toJSON();
            });

            this.listenTo(this.model, 'willSave', this.completeCurrentTag, this);
        },

        render: function () {
            var tags = this.model.get('tags'),
                $tags = $('.tags'),
                tagOffset,
                self = this;

            $tags.empty();

            if (tags) {
                _.forEach(tags, function (tag) {
                    var $tag = $('' + tag.name + '');
                    $tags.append($tag);
                    $("[data-tag-id=" + tag.id + "]")[0].scrollIntoView(true);
                });
            }

            this.$suggestions = $("ul.suggestions").hide(); // Initialise suggestions overlay

            if ($tags.length) {
                tagOffset = $('.tag-input').offset().left;
                $('.tag-blocks').css({'left': tagOffset + 'px'});
            }

            $(window).on('resize', self.resize).trigger('resize');

            $('.tag-label').on('touchstart', function () {
                $(this).addClass('touch');
            });

            return this;
        },

        mobileTags: function () {
            var mq = window.matchMedia("(max-width: 400px)"),
                publishBar = $("#publish-bar");
            if (mq.matches) {

                if (publishBar.hasClass("extended-tags")) {
                    publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
                        $(this).removeClass("extended-tags");
                        $(".tag-input").blur();
                    });
                } else {
                    publishBar.animate({"top": 0, "height": $(window).height()}, 300, "swing", function () {
                        $(this).addClass("extended-tags");
                        $(".tag-input").focus();
                    });
                }

                $(".tag-input").one("blur", function () {

                    if (publishBar.hasClass("extended-tags") && !$(':hover').last().hasClass("tag")) {
                        publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
                            $(this).removeClass("extended-tags");
                            $(document.activeElement).blur();
                            document.documentElement.style.display = "none";
                            setTimeout(function () { document.documentElement.style.display = 'block'; }, 0);
                        });
                    }
                });

                window.scrollTo(0, 1);
            }
        },

        showSuggestions: function ($target, _searchTerm) {
            this.$suggestions.show();
            var searchTerm = _searchTerm.toLowerCase(),
                matchingTags = this.findMatchingTags(searchTerm),
                styles = {
                    left: $target.position().left
                },
                maxSuggestions = 5, // Limit the suggestions number
                regexTerm = searchTerm.replace(/(\s+)/g, "(<[^>]+>)*$1(<[^>]+>)*"),
                regexPattern = new RegExp("(" + regexTerm + ")", "i");

            this.$suggestions.css(styles);
            this.$suggestions.html("");

            matchingTags = _.first(matchingTags, maxSuggestions);
            _.each(matchingTags, function (matchingTag) {
                var highlightedName,
                    suggestionHTML;

                highlightedName = matchingTag.name.replace(regexPattern, "$1");
                /*jslint regexp: true */ // - would like to remove this
                highlightedName = highlightedName.replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1$2$4");

                suggestionHTML = "
" + highlightedName + "
";
                this.$suggestions.append(suggestionHTML);
            }, this);
        },

        handleKeyup: function (e) {
            var $target = $(e.currentTarget),
                searchTerm = $.trim($target.val()),
                tag,
                $selectedSuggestion;

            if (e.keyCode === this.keys.UP) {
                e.preventDefault();
                if (this.$suggestions.is(":visible")) {
                    if (this.$suggestions.children(".selected").length === 0) {
                        this.$suggestions.find("li:last-child").addClass('selected');
                    } else {
                        this.$suggestions.children(".selected").removeClass('selected').prev().addClass('selected');
                    }
                }
            } else if (e.keyCode === this.keys.DOWN) {
                e.preventDefault();
                if (this.$suggestions.is(":visible")) {
                    if (this.$suggestions.children(".selected").length === 0) {
                        this.$suggestions.find("li:first-child").addClass('selected');
                    } else {
                        this.$suggestions.children(".selected").removeClass('selected').next().addClass('selected');
                    }
                }
            } else if (e.keyCode === this.keys.ESC) {
                this.$suggestions.hide();
            } else if ((e.keyCode === this.keys.ENTER || e.keyCode === this.keys.COMMA) && searchTerm) {
                // Submit tag using enter or comma key
                e.preventDefault();

                $selectedSuggestion = this.$suggestions.children(".selected");
                if (this.$suggestions.is(":visible") && $selectedSuggestion.length !== 0) {

                    if ($('.tag:containsExact("' + $selectedSuggestion.data('tag-name') + '")').length === 0) {
                        tag = {id: $selectedSuggestion.data('tag-id'), name: $selectedSuggestion.data('tag-name')};
                        this.addTag(tag);
                    }
                } else {
                    if (e.keyCode === this.keys.COMMA) {
                        searchTerm = searchTerm.replace(/,/g, "");
                    }  // Remove comma from string if comma is used to submit.
                    if ($('.tag:containsExact("' + searchTerm + '")').length === 0) {
                        this.addTag({id: null, name: searchTerm});
                    }
                }
                $target.val('').focus();
                searchTerm = ""; // Used to reset search term
                this.$suggestions.hide();
            }

            if (e.keyCode === this.keys.UP || e.keyCode === this.keys.DOWN) {
                return false;
            }

            if (searchTerm) {
                this.showSuggestions($target, searchTerm);
            } else {
                this.$suggestions.hide();
            }
        },

        handleKeydown: function (e) {
            var $target = $(e.currentTarget),
                lastBlock,
                tag;
            // Delete character tiggers on Keydown, so needed to check on that event rather than Keyup.
            if (e.keyCode === this.keys.BACKSPACE && !$target.val()) {
                lastBlock = this.$('.tags').find('.tag').last();
                lastBlock.remove();
                tag = {id: lastBlock.data('tag-id'), name: lastBlock.text()};
                this.model.removeTag(tag);
            }
        },

        completeCurrentTag: function () {
            var $target = this.$('.tag-input'),
                tagName = $target.val(),
                usedTagNames,
                hasAlreadyBeenAdded;

            usedTagNames = _.map(this.model.get('tags'), function (tag) {
                return tag.name.toUpperCase();
            });
            hasAlreadyBeenAdded = usedTagNames.indexOf(tagName.toUpperCase()) !== -1;

            if (tagName.length > 0 && !hasAlreadyBeenAdded) {
                this.addTag({id: null, name: tagName});
            }
        },

        handleSuggestionClick: function (e) {
            var $target = $(e.currentTarget);
            if (e) { e.preventDefault(); }
            this.addTag({id: $target.data('tag-id'), name: $target.data('tag-name')});
        },

        handleTagClick: function (e) {
            var $tag = $(e.currentTarget),
                tag = {id: $tag.data('tag-id'), name: $tag.text()};
            $tag.remove();
            window.scrollTo(0, 1);
            this.model.removeTag(tag);
        },

        resize: _.throttle(function () {
            var $tags = $('.tags');
            if ($(window).width() > 400) {
                $tags.css("max-width", $("#entry-tags").width() - 320);
            } else {
                $tags.css("max-width", "inherit");
            }
        }, 50),

        findMatchingTags: function (searchTerm) {
            var matchingTagModels,
                self = this;

            if (!this.allGhostTags) {
                return [];
            }

            searchTerm = searchTerm.toUpperCase();
            matchingTagModels = _.filter(this.allGhostTags, function (tag) {
                var tagNameMatches,
                    hasAlreadyBeenAdded;

                tagNameMatches = tag.name.toUpperCase().indexOf(searchTerm) !== -1;

                hasAlreadyBeenAdded = _.some(self.model.get('tags'), function (usedTag) {
                    return tag.name.toUpperCase() === usedTag.name.toUpperCase();
                });
                return tagNameMatches && !hasAlreadyBeenAdded;
            });

            return matchingTagModels;
        },

        addTag: function (tag) {
            var $tag = $('' + tag.name + '');
            this.$('.tags').append($tag);
            $(".tag").last()[0].scrollIntoView(true);
            window.scrollTo(0, 1);
            this.model.addTag(tag);

            this.$('.tag-input').val('').focus();
            this.$suggestions.hide();
        }
    });

}());
<commit_msg>Improved comma tag completion on international keyboard layouts<commit_after>// The Tag UI area associated with a post

/*global window, document, setTimeout, $, _, Backbone, Ghost */

(function () {
    "use strict";

    Ghost.View.EditorTagWidget = Ghost.View.extend({

        events: {
            'keyup [data-input-behaviour="tag"]': 'handleKeyup',
            'keydown [data-input-behaviour="tag"]': 'handleKeydown',
            'keypress [data-input-behaviour="tag"]': 'handleKeypress',
            'click ul.suggestions li': 'handleSuggestionClick',
            'click .tags .tag': 'handleTagClick',
            'click .tag-label': 'mobileTags'
        },

        keys: {
            UP: 38,
            DOWN: 40,
            ESC: 27,
            ENTER: 13,
            BACKSPACE: 8
        },

        initialize: function () {
            var self = this,
                tagCollection = new Ghost.Collections.Tags();

            tagCollection.fetch().then(function () {
                self.allGhostTags = tagCollection.toJSON();
            });

            this.listenTo(this.model, 'willSave', this.completeCurrentTag, this);
        },

        render: function () {
            var tags = this.model.get('tags'),
                $tags = $('.tags'),
                tagOffset,
                self = this;

            $tags.empty();

            if (tags) {
                _.forEach(tags, function (tag) {
                    var $tag = $('' + tag.name + '');
                    $tags.append($tag);
                    $("[data-tag-id=" + tag.id + "]")[0].scrollIntoView(true);
                });
            }

            this.$suggestions = $("ul.suggestions").hide(); // Initialise suggestions overlay

            if ($tags.length) {
                tagOffset = $('.tag-input').offset().left;
                $('.tag-blocks').css({'left': tagOffset + 'px'});
            }

            $(window).on('resize', self.resize).trigger('resize');

            $('.tag-label').on('touchstart', function () {
                $(this).addClass('touch');
            });

            return this;
        },

        mobileTags: function () {
            var mq = window.matchMedia("(max-width: 400px)"),
                publishBar = $("#publish-bar");
            if (mq.matches) {

                if (publishBar.hasClass("extended-tags")) {
                    publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
                        $(this).removeClass("extended-tags");
                        $(".tag-input").blur();
                    });
                } else {
                    publishBar.animate({"top": 0, "height": $(window).height()}, 300, "swing", function () {
                        $(this).addClass("extended-tags");
                        $(".tag-input").focus();
                    });
                }

                $(".tag-input").one("blur", function () {

                    if (publishBar.hasClass("extended-tags") && !$(':hover').last().hasClass("tag")) {
                        publishBar.css("top", "auto").animate({"height": "40px"}, 300, "swing", function () {
                            $(this).removeClass("extended-tags");
                            $(document.activeElement).blur();
                            document.documentElement.style.display = "none";
                            setTimeout(function () { document.documentElement.style.display = 'block'; }, 0);
                        });
                    }
                });

                window.scrollTo(0, 1);
            }
        },

        showSuggestions: function ($target, _searchTerm) {
            this.$suggestions.show();
            var searchTerm = _searchTerm.toLowerCase(),
                matchingTags = this.findMatchingTags(searchTerm),
                styles = {
                    left: $target.position().left
                },
                maxSuggestions = 5, // Limit the suggestions number
                regexTerm = searchTerm.replace(/(\s+)/g, "(<[^>]+>)*$1(<[^>]+>)*"),
                regexPattern = new RegExp("(" + regexTerm + ")", "i");

            this.$suggestions.css(styles);
            this.$suggestions.html("");

            matchingTags = _.first(matchingTags, maxSuggestions);
            _.each(matchingTags, function (matchingTag) {
                var highlightedName,
                    suggestionHTML;

                highlightedName = matchingTag.name.replace(regexPattern, "$1");
                /*jslint regexp: true */ // - would like to remove this
                highlightedName = highlightedName.replace(/([^<>]*)((<[^>]+>)+)([^<>]*<\/mark>)/, "$1$2$4");

                suggestionHTML = "
" + highlightedName + "
";
                this.$suggestions.append(suggestionHTML);
            }, this);
        },

        handleKeyup: function (e) {
            var $target = $(e.currentTarget),
                searchTerm = $.trim($target.val());

            if (e.keyCode === this.keys.UP) {
                e.preventDefault();
                if (this.$suggestions.is(":visible")) {
                    if (this.$suggestions.children(".selected").length === 0) {
                        this.$suggestions.find("li:last-child").addClass('selected');
                    } else {
                        this.$suggestions.children(".selected").removeClass('selected').prev().addClass('selected');
                    }
                }
            } else if (e.keyCode === this.keys.DOWN) {
                e.preventDefault();
                if (this.$suggestions.is(":visible")) {
                    if (this.$suggestions.children(".selected").length === 0) {
                        this.$suggestions.find("li:first-child").addClass('selected');
                    } else {
                        this.$suggestions.children(".selected").removeClass('selected').next().addClass('selected');
                    }
                }
            } else if (e.keyCode === this.keys.ESC) {
                this.$suggestions.hide();
            }

            if (e.keyCode === this.keys.UP || e.keyCode === this.keys.DOWN) {
                return false;
            }

            if (searchTerm) {
                this.showSuggestions($target, searchTerm);
            } else {
                this.$suggestions.hide();
            }
        },

        handleKeydown: function (e) {
            var $target = $(e.currentTarget),
                lastBlock,
                tag;
            // Delete character tiggers on Keydown, so needed to check on that event rather than Keyup.
            if (e.keyCode === this.keys.BACKSPACE && !$target.val()) {
                lastBlock = this.$('.tags').find('.tag').last();
                lastBlock.remove();
                tag = {id: lastBlock.data('tag-id'), name: lastBlock.text()};
                this.model.removeTag(tag);
            }
        },

        handleKeypress: function (e) {
            var $target = $(e.currentTarget),
                searchTerm = $.trim($target.val()),
                tag,
                $selectedSuggestion,
                isComma = ",".localeCompare(String.fromCharCode(e.keyCode || e.charCode)) === 0;

            // use localeCompare in case of international keyboard layout
            if ((e.keyCode === this.keys.ENTER || isComma) && searchTerm) {
                // Submit tag using enter or comma key
                e.preventDefault();

                $selectedSuggestion = this.$suggestions.children(".selected");
                if (this.$suggestions.is(":visible") && $selectedSuggestion.length !== 0) {

                    if ($('.tag:containsExact("' + $selectedSuggestion.data('tag-name') + '")').length === 0) {
                        tag = {id: $selectedSuggestion.data('tag-id'), name: $selectedSuggestion.data('tag-name')};
                        this.addTag(tag);
                    }
                } else {
                    if (isComma) {
                        searchTerm = searchTerm.replace(/,/g, "");
                    }  // Remove comma from string if comma is used to submit.
                    if ($('.tag:containsExact("' + searchTerm + '")').length === 0) {
                        this.addTag({id: null, name: searchTerm});
                    }
                }
                $target.val('').focus();
                searchTerm = ""; // Used to reset search term
                this.$suggestions.hide();
            }
        },

        completeCurrentTag: function () {
            var $target = this.$('.tag-input'),
                tagName = $target.val(),
                usedTagNames,
                hasAlreadyBeenAdded;

            usedTagNames = _.map(this.model.get('tags'), function (tag) {
                return tag.name.toUpperCase();
            });
            hasAlreadyBeenAdded = usedTagNames.indexOf(tagName.toUpperCase()) !== -1;

            if (tagName.length > 0 && !hasAlreadyBeenAdded) {
                this.addTag({id: null, name: tagName});
            }
        },

        handleSuggestionClick: function (e) {
            var $target = $(e.currentTarget);
            if (e) { e.preventDefault(); }
            this.addTag({id: $target.data('tag-id'), name: $target.data('tag-name')});
        },

        handleTagClick: function (e) {
            var $tag = $(e.currentTarget),
                tag = {id: $tag.data('tag-id'), name: $tag.text()};
            $tag.remove();
            window.scrollTo(0, 1);
            this.model.removeTag(tag);
        },

        resize: _.throttle(function () {
            var $tags = $('.tags');
            if ($(window).width() > 400) {
                $tags.css("max-width", $("#entry-tags").width() - 320);
            } else {
                $tags.css("max-width", "inherit");
            }
        }, 50),

        findMatchingTags: function (searchTerm) {
            var matchingTagModels,
                self = this;

            if (!this.allGhostTags) {
                return [];
            }

            searchTerm = searchTerm.toUpperCase();
            matchingTagModels = _.filter(this.allGhostTags, function (tag) {
                var tagNameMatches,
                    hasAlreadyBeenAdded;

                tagNameMatches = tag.name.toUpperCase().indexOf(searchTerm) !== -1;

                hasAlreadyBeenAdded = _.some(self.model.get('tags'), function (usedTag) {
                    return tag.name.toUpperCase() === usedTag.name.toUpperCase();
                });
                return tagNameMatches && !hasAlreadyBeenAdded;
            });

            return matchingTagModels;
        },

        addTag: function (tag) {
            var $tag = $('' + tag.name + '');
            this.$('.tags').append($tag);
            $(".tag").last()[0].scrollIntoView(true);
            window.scrollTo(0, 1);
            this.model.addTag(tag);

            this.$('.tag-input').val('').focus();
            this.$suggestions.hide();
        }
    });

}());
<|endoftext|>


<commit_before>import _ from 'underscore';

import RestfulModel from './restful-model';
import Attributes from './attributes';

class EmailAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      email: this.email,
    };
    return json;
  }
}
EmailAddress.collectionName = 'email_addresses';
EmailAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  email: Attributes.String({
    modelKey: 'email',
  }),
});

class IMAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      im_address: this.im_address,
    };
    return json;
  }
}
IMAddress.collectionName = 'im_addresses';
IMAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  imAddress: Attributes.String({
    modelKey: 'imAddress',
    jsonKey: 'im_address',
  }),
});

class PhysicalAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      format: this.format,
    };
    if (this.format == 'unstructured') {
      json.address = this.address;
    } else {
      json.street_address = this.streetAddress;
      json.postal_code = this.postalCode;
      json.state = this.state;
      json.city = this.city;
      json.country = this.country;
    }
    return json;
  }
}
PhysicalAddress.collectionName = 'physical_addresses';
PhysicalAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  format: Attributes.String({
    modelKey: 'format',
  }),
  address: Attributes.String({
    modelKey: 'address',
  }),
  streetAddress: Attributes.String({
    modelKey: 'streetAddress',
  }),
  city: Attributes.String({
    modelKey: 'city',
  }),
  postalCode: Attributes.String({
    modelKey: 'postalCode',
  }),
  state: Attributes.String({
    modelKey: 'state',
  }),
  country: Attributes.String({
    modelKey: 'country',
  }),
});

class PhoneNumber extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      number: this.number,
    };
    return json;
  }
}

PhoneNumber.collectionName = 'phone_numbers';
PhoneNumber.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  number: Attributes.String({
    modelKey: 'number',
  }),
});

class WebPage extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      url: this.url,
    };
    return json;
  }
}
WebPage.collectionName = 'web_pages';
WebPage.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  url: Attributes.String({
    modelKey: 'url',
  }),
});

export default class Contact extends RestfulModel {
  save(params = {}, callback = null) {
    return this._save(params, callback);
  }

  getPicture(params = {}, callback = null) {
    return this._get(params, callback, '/picture');
  }
}
Contact.collectionName = 'contacts';
Contact.attributes = _.extend({}, RestfulModel.attributes, {
  givenName: Attributes.String({
    modelKey: 'givenName',
    jsonKey: 'given_name',
  }),
  middleName: Attributes.String({
    modelKey: 'middleName',
    jsonKey: 'middle_name',
  }),
  surname: Attributes.String({
    modelKey: 'surname',
  }),
  suffix: Attributes.String({
    modelKey: 'suffix',
  }),
  nickname: Attributes.String({
    modelKey: 'nickname',
  }),
  birthday: Attributes.Date({
    modelKey: 'birthday',
  }),
  companyName: Attributes.String({
    modelKey: 'companyName',
    jsonKey: 'company_name',
  }),
  jobTitle: Attributes.String({
    modelKey: 'jobTitle',
    jsonKey: 'job_title',
  }),
  managerName: Attributes.String({
    modelKey: 'managerName',
    jsonKey: 'manager_name',
  }),
  officeLocation: Attributes.String({
    modelKey: 'officeLocation',
    jsonKey: 'office_location',
  }),
  notes: Attributes.String({
    modelKey: 'notes',
  }),
  pictureUrl: Attributes.String({
    modelKey: 'pictureUrl',
    jsonKey: 'picture_url',
  }),
  emailAddresses: Attributes.Collection({
    modelKey: 'emailAddresses',
    jsonKey: 'email_addresses',
    itemClass: EmailAddress,
  }),
  imAddresses: Attributes.Collection({
    modelKey: 'imAddresses',
    jsonKey: 'im_addresses',
    itemClass: IMAddress,
  }),
  physicalAddresses: Attributes.Collection({
    modelKey: 'physicalAddresses',
    jsonKey: 'physical_addresses',
    itemClass: PhysicalAddress,
  }),
  phoneNumbers: Attributes.Collection({
    modelKey: 'phoneNumbers',
    jsonKey: 'phone_numbers',
    itemClass: PhoneNumber,
  }),
  webPages: Attributes.Collection({
    modelKey: 'webPages',
    jsonKey: 'web_pages',
    itemClass: WebPage,
  }),
});
<commit_msg>Add json keys to physical address attributes<commit_after>import _ from 'underscore';

import RestfulModel from './restful-model';
import Attributes from './attributes';

class EmailAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      email: this.email,
    };
    return json;
  }
}
EmailAddress.collectionName = 'email_addresses';
EmailAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  email: Attributes.String({
    modelKey: 'email',
  }),
});

class IMAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      im_address: this.im_address,
    };
    return json;
  }
}
IMAddress.collectionName = 'im_addresses';
IMAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  imAddress: Attributes.String({
    modelKey: 'imAddress',
    jsonKey: 'im_address',
  }),
});

class PhysicalAddress extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      format: this.format,
    };
    if (this.format == 'unstructured') {
      json.address = this.address;
    } else {
      json.street_address = this.streetAddress;
      json.postal_code = this.postalCode;
      json.state = this.state;
      json.city = this.city;
      json.country = this.country;
    }
    return json;
  }
}
PhysicalAddress.collectionName = 'physical_addresses';
PhysicalAddress.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  format: Attributes.String({
    modelKey: 'format',
  }),
  address: Attributes.String({
    modelKey: 'address',
  }),
  streetAddress: Attributes.String({
    modelKey: 'streetAddress',
    jsonKey: 'street_address',
  }),
  city: Attributes.String({
    modelKey: 'city',
  }),
  postalCode: Attributes.String({
    modelKey: 'postalCode',
    jsonKey: 'postal_code',
  }),
  state: Attributes.String({
    modelKey: 'state',
  }),
  country: Attributes.String({
    modelKey: 'country',
  }),
});

class PhoneNumber extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      number: this.number,
    };
    return json;
  }
}

PhoneNumber.collectionName = 'phone_numbers';
PhoneNumber.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  number: Attributes.String({
    modelKey: 'number',
  }),
});

class WebPage extends RestfulModel {
  toJSON() {
    const json = {
      type: this.type,
      url: this.url,
    };
    return json;
  }
}
WebPage.collectionName = 'web_pages';
WebPage.attributes = _.extend({}, RestfulModel.attributes, {
  type: Attributes.String({
    modelKey: 'type',
  }),
  url: Attributes.String({
    modelKey: 'url',
  }),
});

export default class Contact extends RestfulModel {
  save(params = {}, callback = null) {
    return this._save(params, callback);
  }

  getPicture(params = {}, callback = null) {
    return this._get(params, callback, '/picture');
  }
}
Contact.collectionName = 'contacts';
Contact.attributes = _.extend({}, RestfulModel.attributes, {
  givenName: Attributes.String({
    modelKey: 'givenName',
    jsonKey: 'given_name',
  }),
  middleName: Attributes.String({
    modelKey: 'middleName',
    jsonKey: 'middle_name',
  }),
  surname: Attributes.String({
    modelKey: 'surname',
  }),
  suffix: Attributes.String({
    modelKey: 'suffix',
  }),
  nickname: Attributes.String({
    modelKey: 'nickname',
  }),
  birthday: Attributes.Date({
    modelKey: 'birthday',
  }),
  companyName: Attributes.String({
    modelKey: 'companyName',
    jsonKey: 'company_name',
  }),
  jobTitle: Attributes.String({
    modelKey: 'jobTitle',
    jsonKey: 'job_title',
  }),
  managerName: Attributes.String({
    modelKey: 'managerName',
    jsonKey: 'manager_name',
  }),
  officeLocation: Attributes.String({
    modelKey: 'officeLocation',
    jsonKey: 'office_location',
  }),
  notes: Attributes.String({
    modelKey: 'notes',
  }),
  pictureUrl: Attributes.String({
    modelKey: 'pictureUrl',
    jsonKey: 'picture_url',
  }),
  emailAddresses: Attributes.Collection({
    modelKey: 'emailAddresses',
    jsonKey: 'email_addresses',
    itemClass: EmailAddress,
  }),
  imAddresses: Attributes.Collection({
    modelKey: 'imAddresses',
    jsonKey: 'im_addresses',
    itemClass: IMAddress,
  }),
  physicalAddresses: Attributes.Collection({
    modelKey: 'physicalAddresses',
    jsonKey: 'physical_addresses',
    itemClass: PhysicalAddress,
  }),
  phoneNumbers: Attributes.Collection({
    modelKey: 'phoneNumbers',
    jsonKey: 'phone_numbers',
    itemClass: PhoneNumber,
  }),
  webPages: Attributes.Collection({
    modelKey: 'webPages',
    jsonKey: 'web_pages',
    itemClass: WebPage,
  }),
});
<|endoftext|>


<commit_before>import Immutable from 'immutable'
import flatten from 'lodash/array/flatten'
import forEach from 'lodash/collection/forEach'
import omit from 'lodash/object/omit'
import {v4 as uuid} from 'node-uuid'
import stringify from 'json-stable-stringify'
import present from 'present'

import {version as currentVersionString} from '../../package.json'
import {studentChangelog as changelog} from '../lib/logger'
import checkGraduatability from '../lib/check-student-graduatability'

import {randomChar} from 'sto-helpers'

import Schedule from './schedule'
import Study from './study'

const now = new Date()
const StudentRecord = Immutable.Record({
	id: null,
	name: null,
	version: currentVersionString,

	creditsNeeded: 35,

	matriculation: now.getFullYear() - 2,
	graduation: now.getFullYear() + 2,
	advisor: '',

	dateLastModified: null,
	dateCreated: null,

	studies: Immutable.OrderedMap(),
	schedules: Immutable.OrderedMap(),
	overrides: Immutable.OrderedMap(),
	fabrications: Immutable.OrderedMap(),

	settings: Immutable.Map(),
})

export default class Student extends StudentRecord {
	constructor(encodedStudent={}) {
		const startTime = present()

		// Don't pass the list params into the StudentRecord constructor;
		// it creates them as JS objects, instead of our custom records.
		const toRemove = ['studies', 'schedules', 'overrides', 'fabrications', 'settings']
		const filtered = omit(encodedStudent, toRemove)
		const immutableStudent = Immutable.fromJS(filtered)

		super(immutableStudent)

		// Then add them manually.
		if (encodedStudent) {
			return this.withMutations((student) => {
				student = student.set('id', encodedStudent.id || uuid())
				student = student.set('name', encodedStudent.name || 'Student ' + randomChar())
				student = student.set('dateCreated', encodedStudent.dateCreated || new Date())
				student = student.set('dateLastModified', encodedStudent.dateLastModified || new Date())
				student = student.set('version', currentVersionString)

				forEach((encodedStudent.studies || []), study => {
					student = student.addArea(study)
				})

				forEach((encodedStudent.schedules || []), schedule => {
					student = student.addSchedule(schedule)
				})

				student = student.setOverride(encodedStudent.overrides || {})

				forEach((encodedStudent.fabrications || []), fabrication => {
					student = student.addFabrication(fabrication)
				})

				forEach((encodedStudent.settings || {}), (value, key) => {
					student = student.changeSetting(key, value)
				})

				changelog(`it took ${present() - startTime} ms to make a student`)

				return student
			})
		}
	}

	changeName(newName) {
		return this.set('name', (newName || '').trim())
	}

	changeAdvisor(newAdvisor) {
		return this.set('advisor', newAdvisor)
	}

	changeCreditsNeeded(newCreditsNeeded) {
		return this.set('creditsNeeded', newCreditsNeeded)
	}

	changeMatriculation(newMatriculation) {
		return this.set('matriculation', newMatriculation)
	}

	changeGraduation(newGraduation) {
		return this.set('graduation', newGraduation)
	}

	changeSetting(key, value) {
		return this.setIn(['settings', key], value)
	}

	get graduatability() {
		return checkGraduatability(this)
	}

	// schedule methods

	addSchedule(newSchedule) {
		const sched = new Schedule(newSchedule)
		return this.setIn(['schedules', sched.id], sched)
	}

	destroySchedule(scheduleId) {
		changelog(`removing schedule ${scheduleId}`)

		const deadSched = this.getIn(['schedules', scheduleId])
		const scheduleIsNoMore = this.set('schedules', this.schedules.delete(scheduleId))

		if (deadSched && deadSched.active) {
			const otherSchedKey = scheduleIsNoMore.findKey((sched) =>
				sched.year === deadSched.year &&
				sched.semester === deadSched.semester &&
				sched.id !== deadSched.id)

			if (otherSchedKey) {
				return scheduleIsNoMore.setIn(['schedules', otherSchedKey, 'active'], true)
			}
		}
		return scheduleIsNoMore
	}

	destroyMultipleSchedules(ids) {
		return this.withMutations((student) => {
			changelog('destroyMultipleSchedules', ids)
			// console.groupCollapsed('destroyMultipleSchedules')
			forEach('toArray' in ids ? ids.toArray() : ids, (id) => {
				student = student.destroySchedule(id)
			})
			// console.groupEnd('destroyMultipleSchedules')
			return student
		})
	}

	moveCourse(fromScheduleId, toScheduleId, clbid) {
		changelog(`moving course ${clbid} from schedule ${fromScheduleId} to schedule ${toScheduleId}`)
		return this.withMutations((student) => {
			student = student.setIn(['schedules', fromScheduleId], student.getIn(['schedules', fromScheduleId]).removeCourse(clbid))
			student = student.setIn(['schedules', toScheduleId], student.getIn(['schedules', toScheduleId]).addCourse(clbid))
		})
	}

	// area-of-study methods

	addArea(areaOfStudy) {
		const study = new Study(areaOfStudy)
		return this.setIn(['studies', study.id], study)
	}

	removeArea(id) {
		return this.set('studies', this.studies.delete(id))
	}

	removeMultipleAreas(ids) {
		return this.withMutations((student) => {
			forEach(ids.hasOwnProperty('toArray') ? ids.toArray() : ids, id => {
				student = student.removeArea(id)
			})
			return student
		})
	}

	// override methods

	setOverride(overrideObj) {
		return this.withMutations(student => {
			forEach(overrideObj, (val, key) => {
				student = student.setIn(['overrides', key], val)
			})
			return student
		})
	}

	removeOverride(thingOverriden) {
		return this.set('overrides', this.overrides.delete(thingOverriden))
	}

	// fabrication methods

	addFabrication(fabrication) {
		return this.setIn(['fabrications', fabrication.id], fabrication)
	}

	removeFabrication(fabricationId) {
		return this.set('fabrications', this.fabrications.delete(fabricationId))
	}

	// getters

	get courses() {
		const allCourses = this.schedules
			.filter(sched => sched.active)
			.map((schedule) => schedule.courses)
			.toArray()
		const scheduleCoursePromises = Promise.all(allCourses)
		return scheduleCoursePromises.then((scheduleCourses) => flatten(scheduleCourses))
	}

	// helpers

	data() {
		return Promise.all([
			this.courses,
			this.creditsNeeded,
			this.fabrications.toList().toJS(),
			this.graduation,
			this.matriculation,
			this.overrides.toObject(),
			this.studies.toList().toJS(),
		]).then(results => ({
			courses: results[0],
			creditsNeeded: results[1],
			fabrications: results[2],
			graduation: results[3],
			matriculation: results[4],
			overrides: results[5],
			studies: results[6],
		}))
	}

	encode() {
		return encodeURIComponent(stringify(this))
	}

	save() {
		// grab the old (still-JSON-encoded) student from localstorage
		// compare it to the current one
		// if they're different, update dateLastModified, stringify, and save.
		const oldVersion = localStorage.getItem(this.id)
		if (oldVersion !== stringify(this)) {
			changelog(`saving student ${this.name} (${this.id})`)
			const student = this.set('dateLastModified', new Date())
			localStorage.setItem(student.id, stringify(student))
			require('../lib/db').stores.students.put(JSON.parse(stringify(student)))
		}
	}

	toJSON() {
		return {
			id: this.id,
			name: this.name,
			version: this.version,
			creditsNeeded: this.creditsNeeded,
			matriculation: this.matriculation,
			graduation: this.graduation,
			studies: this.studies.toList(),
			schedules: this.schedules.toList(),
			overrides: this.overrides,
			fabrications: this.fabrications.toList(),
			settings: this.settings,
		}
	}
}
<commit_msg>don't trim the student name field<commit_after>import Immutable from 'immutable'
import flatten from 'lodash/array/flatten'
import forEach from 'lodash/collection/forEach'
import omit from 'lodash/object/omit'
import {v4 as uuid} from 'node-uuid'
import stringify from 'json-stable-stringify'
import present from 'present'

import {version as currentVersionString} from '../../package.json'
import {studentChangelog as changelog} from '../lib/logger'
import checkGraduatability from '../lib/check-student-graduatability'

import {randomChar} from 'sto-helpers'

import Schedule from './schedule'
import Study from './study'

const now = new Date()
const StudentRecord = Immutable.Record({
	id: null,
	name: null,
	version: currentVersionString,

	creditsNeeded: 35,

	matriculation: now.getFullYear() - 2,
	graduation: now.getFullYear() + 2,
	advisor: '',

	dateLastModified: null,
	dateCreated: null,

	studies: Immutable.OrderedMap(),
	schedules: Immutable.OrderedMap(),
	overrides: Immutable.OrderedMap(),
	fabrications: Immutable.OrderedMap(),

	settings: Immutable.Map(),
})

export default class Student extends StudentRecord {
	constructor(encodedStudent={}) {
		const startTime = present()

		// Don't pass the list params into the StudentRecord constructor;
		// it creates them as JS objects, instead of our custom records.
		const toRemove = ['studies', 'schedules', 'overrides', 'fabrications', 'settings']
		const filtered = omit(encodedStudent, toRemove)
		const immutableStudent = Immutable.fromJS(filtered)

		super(immutableStudent)

		// Then add them manually.
		if (encodedStudent) {
			return this.withMutations((student) => {
				student = student.set('id', encodedStudent.id || uuid())
				student = student.set('name', encodedStudent.name || 'Student ' + randomChar())
				student = student.set('dateCreated', encodedStudent.dateCreated || new Date())
				student = student.set('dateLastModified', encodedStudent.dateLastModified || new Date())
				student = student.set('version', currentVersionString)

				forEach((encodedStudent.studies || []), study => {
					student = student.addArea(study)
				})

				forEach((encodedStudent.schedules || []), schedule => {
					student = student.addSchedule(schedule)
				})

				student = student.setOverride(encodedStudent.overrides || {})

				forEach((encodedStudent.fabrications || []), fabrication => {
					student = student.addFabrication(fabrication)
				})

				forEach((encodedStudent.settings || {}), (value, key) => {
					student = student.changeSetting(key, value)
				})

				changelog(`it took ${present() - startTime} ms to make a student`)

				return student
			})
		}
	}

	changeName(newName) {
		return this.set('name', newName)
	}

	changeAdvisor(newAdvisor) {
		return this.set('advisor', newAdvisor)
	}

	changeCreditsNeeded(newCreditsNeeded) {
		return this.set('creditsNeeded', newCreditsNeeded)
	}

	changeMatriculation(newMatriculation) {
		return this.set('matriculation', newMatriculation)
	}

	changeGraduation(newGraduation) {
		return this.set('graduation', newGraduation)
	}

	changeSetting(key, value) {
		return this.setIn(['settings', key], value)
	}

	get graduatability() {
		return checkGraduatability(this)
	}

	// schedule methods

	addSchedule(newSchedule) {
		const sched = new Schedule(newSchedule)
		return this.setIn(['schedules', sched.id], sched)
	}

	destroySchedule(scheduleId) {
		changelog(`removing schedule ${scheduleId}`)

		const deadSched = this.getIn(['schedules', scheduleId])
		const scheduleIsNoMore = this.set('schedules', this.schedules.delete(scheduleId))

		if (deadSched && deadSched.active) {
			const otherSchedKey = scheduleIsNoMore.findKey((sched) =>
				sched.year === deadSched.year &&
				sched.semester === deadSched.semester &&
				sched.id !== deadSched.id)

			if (otherSchedKey) {
				return scheduleIsNoMore.setIn(['schedules', otherSchedKey, 'active'], true)
			}
		}
		return scheduleIsNoMore
	}

	destroyMultipleSchedules(ids) {
		return this.withMutations((student) => {
			changelog('destroyMultipleSchedules', ids)
			// console.groupCollapsed('destroyMultipleSchedules')
			forEach('toArray' in ids ? ids.toArray() : ids, (id) => {
				student = student.destroySchedule(id)
			})
			// console.groupEnd('destroyMultipleSchedules')
			return student
		})
	}

	moveCourse(fromScheduleId, toScheduleId, clbid) {
		changelog(`moving course ${clbid} from schedule ${fromScheduleId} to schedule ${toScheduleId}`)
		return this.withMutations((student) => {
			student = student.setIn(['schedules', fromScheduleId], student.getIn(['schedules', fromScheduleId]).removeCourse(clbid))
			student = student.setIn(['schedules', toScheduleId], student.getIn(['schedules', toScheduleId]).addCourse(clbid))
		})
	}

	// area-of-study methods

	addArea(areaOfStudy) {
		const study = new Study(areaOfStudy)
		return this.setIn(['studies', study.id], study)
	}

	removeArea(id) {
		return this.set('studies', this.studies.delete(id))
	}

	removeMultipleAreas(ids) {
		return this.withMutations((student) => {
			forEach(ids.hasOwnProperty('toArray') ? ids.toArray() : ids, id => {
				student = student.removeArea(id)
			})
			return student
		})
	}

	// override methods

	setOverride(overrideObj) {
		return this.withMutations(student => {
			forEach(overrideObj, (val, key) => {
				student = student.setIn(['overrides', key], val)
			})
			return student
		})
	}

	removeOverride(thingOverriden) {
		return this.set('overrides', this.overrides.delete(thingOverriden))
	}

	// fabrication methods

	addFabrication(fabrication) {
		return this.setIn(['fabrications', fabrication.id], fabrication)
	}

	removeFabrication(fabricationId) {
		return this.set('fabrications', this.fabrications.delete(fabricationId))
	}

	// getters

	get courses() {
		const allCourses = this.schedules
			.filter(sched => sched.active)
			.map((schedule) => schedule.courses)
			.toArray()
		const scheduleCoursePromises = Promise.all(allCourses)
		return scheduleCoursePromises.then((scheduleCourses) => flatten(scheduleCourses))
	}

	// helpers

	data() {
		return Promise.all([
			this.courses,
			this.creditsNeeded,
			this.fabrications.toList().toJS(),
			this.graduation,
			this.matriculation,
			this.overrides.toObject(),
			this.studies.toList().toJS(),
		]).then(results => ({
			courses: results[0],
			creditsNeeded: results[1],
			fabrications: results[2],
			graduation: results[3],
			matriculation: results[4],
			overrides: results[5],
			studies: results[6],
		}))
	}

	encode() {
		return encodeURIComponent(stringify(this))
	}

	save() {
		// grab the old (still-JSON-encoded) student from localstorage
		// compare it to the current one
		// if they're different, update dateLastModified, stringify, and save.
		const oldVersion = localStorage.getItem(this.id)
		if (oldVersion !== stringify(this)) {
			changelog(`saving student ${this.name} (${this.id})`)
			const student = this.set('dateLastModified', new Date())
			localStorage.setItem(student.id, stringify(student))
			require('../lib/db').stores.students.put(JSON.parse(stringify(student)))
		}
	}

	toJSON() {
		return {
			id: this.id,
			name: this.name,
			version: this.version,
			creditsNeeded: this.creditsNeeded,
			matriculation: this.matriculation,
			graduation: this.graduation,
			studies: this.studies.toList(),
			schedules: this.schedules.toList(),
			overrides: this.overrides,
			fabrications: this.fabrications.toList(),
			settings: this.settings,
		}
	}
}
<|endoftext|>


<commit_before>if(!window.multigraph) {
    window.multigraph = {};
}

if(!window.multigraph.ModelTool) {
    window.multigraph.ModelTool = {};
}

(function (ns) {
    "use strict";
    function Spec(n) {
        var that = this,
        name = n,
        methods = {},
        attributes = {},
        pattern,
        Method = window.multigraph.ModelTool.Method;

        this.hasA = function (attr) {
            var attribute;
            if (typeof(attr) === 'string') {
                //this[attr] = new ns.Attr(attr);
                attribute = new ns.Attr(attr);
                attributes[attr] = attribute;
                return attribute;
            } else {
                throw new Error("Spec: hasA parameter must be a string");
            }
        };

        this.hasAn = this.hasA;

        this.hasMany = function (attrs) {
            var attribute;
            if(typeof(attrs) === 'string') {
                //this[attrs] = new ns.AttrList(attrs);
                attribute = new ns.AttrList(attrs);
                attributes[attrs] = attribute;
                return attribute;
                //return this[attrs];
            } else {
                throw new Error("Spec: hasMany parameter must be a string");
            }
        };

        this.attribute = function (attr) {
            var result;

            if (typeof(attr) !== "string") {
                throw new Error("Spec: expected string argument to attribute method, but recieved " + attr);
            }

            result = attributes[attr];

            if (result === undefined) {
                throw new Error("Spec: attribute " + attr + " does not exist!");
            }
            return result;
        }

        this.method = function (m) {
            var result;

            if (typeof(m) !== "string") {
                throw new Error("Spec: expected string argument to method method, but recieved " + m);
            }

            result = methods[m];

            if(result === undefined) {
                throw new Error("Spec: method " + m + " does not exist!");
            }

            return result;
        }

        this.buildsWith = function () {
            var optionalArgs,
            requiredArgs,
            i;

            if (arguments.length === 0) {
                requiredArgs = [];
                optionalArgs = [];
                return this;//What should occur when no arguments are passed to the function?
            }
            var optionalParamFlag = false; //set to true when the first optional parameter is found
                                           //checks for proper grouping of optional parameters
            for (i=0; i<arguments.length-1; i++) {
                if (typeof(arguments[i]) !== 'string') {
                    throw new Error("Spec: buildsWith parameters must be strings except for a function as the optional final parameter");
                } else if (optionalParamFlag === true && arguments[i].charAt(0) !== '%') {
                    throw new Error("Spec: buildsWith requires parameters preceded with a % to be the final parameters before the optional function");
                } else if (optionalParamFlag === false && arguments[i].charAt(0) === '%') {
                    optionalParamFlag = true;
                }
            }
            if (typeof(arguments[arguments.length-1]) === 'string') {
                if (optionalParamFlag === true && arguments[arguments.length-1].charAt(0) !== '%') {
                    throw new Error("Spec: buildsWith requires parameters preceded with a % to be the final parameters before the optional function");
                } else {
                    for (i=0; i<arguments.length; i++) {
                        requiredArgs = [];
                        optionalArgs = [];
                        if (arguments[i].charAt(0) !== '%') {
                            requiredArgs.push(arguments[i]);
                        } else if (arguments[i].charAt(0) === '%') {
                            optionalArgs.push(arguments[i].slice(1));
                        }
                    }
                    return this;
                }
            } else if (typeof(arguments[arguments.length-1]) === 'function') {
                requiredArgs = [];
                optionalArgs = [];
                for (i=0; i<arguments.length-1; i++) {
                    if (arguments[i].charAt(0) !== '%') {
                        requiredArgs.push(arguments[i]);
                    } else if (arguments[i].charAt(0) === '%') {
                        optionalArgs.push(arguments[i].slice(1));
                    }
                }
                arguments[arguments.length-1]();
                return this;
            } else {
                throw new Error("Spec: buildsWith parameters must be strings except for a function as the optional final parameter");
            }
        };

        this.looksLike = function (p) {
            pattern = p;
        };

        this.respondsTo = function (methodName, methodBody) {
            var m = new Method(methodName, methodBody);
            methods[methodName] = m;
        };

        this.create = function (name) {
            this[name] = function () {
                var i;

                //add attributes
                for(i in attributes) {
                    if(attributes.hasOwnProperty(i)) {
                        attributes[i].addTo(this);
                    }
                }

                //add methods
                for(i in methods) {
                    if(methods.hasOwnProperty(i)) {
                        methods[i].addTo(this);
                    }
                }

                this.toString = pattern;
            };
            return this[name];
        };
    }
    ns.Spec = Spec;
}(window.multigraph.ModelTool));<commit_msg>fix lint error from previous commit<commit_after>if(!window.multigraph) {
    window.multigraph = {};
}

if(!window.multigraph.ModelTool) {
    window.multigraph.ModelTool = {};
}

(function (ns) {
    "use strict";
    function Spec(n) {
        var that = this,
        name = n,
        methods = {},
        attributes = {},
        pattern,
        Method = window.multigraph.ModelTool.Method;

        this.hasA = function (attr) {
            var attribute;
            if (typeof(attr) === 'string') {
                //this[attr] = new ns.Attr(attr);
                attribute = new ns.Attr(attr);
                attributes[attr] = attribute;
                return attribute;
            } else {
                throw new Error("Spec: hasA parameter must be a string");
            }
        };

        this.hasAn = this.hasA;

        this.hasMany = function (attrs) {
            var attribute;
            if(typeof(attrs) === 'string') {
                //this[attrs] = new ns.AttrList(attrs);
                attribute = new ns.AttrList(attrs);
                attributes[attrs] = attribute;
                return attribute;
                //return this[attrs];
            } else {
                throw new Error("Spec: hasMany parameter must be a string");
            }
        };

        this.attribute = function (attr) {
            var result;

            if (typeof(attr) !== "string") {
                throw new Error("Spec: expected string argument to attribute method, but recieved " + attr);
            }

            result = attributes[attr];

            if (result === undefined) {
                throw new Error("Spec: attribute " + attr + " does not exist!");
            }
            return result;
        };

        this.method = function (m) {
            var result;

            if (typeof(m) !== "string") {
                throw new Error("Spec: expected string argument to method method, but recieved " + m);
            }

            result = methods[m];

            if(result === undefined) {
                throw new Error("Spec: method " + m + " does not exist!");
            }

            return result;
        };

        this.buildsWith = function () {
            var optionalArgs,
            requiredArgs,
            i;

            if (arguments.length === 0) {
                requiredArgs = [];
                optionalArgs = [];
                return this;//What should occur when no arguments are passed to the function?
            }
            var optionalParamFlag = false; //set to true when the first optional parameter is found
                                           //checks for proper grouping of optional parameters
            for (i=0; i<arguments.length-1; i++) {
                if (typeof(arguments[i]) !== 'string') {
                    throw new Error("Spec: buildsWith parameters must be strings except for a function as the optional final parameter");
                } else if (optionalParamFlag === true && arguments[i].charAt(0) !== '%') {
                    throw new Error("Spec: buildsWith requires parameters preceded with a % to be the final parameters before the optional function");
                } else if (optionalParamFlag === false && arguments[i].charAt(0) === '%') {
                    optionalParamFlag = true;
                }
            }
            if (typeof(arguments[arguments.length-1]) === 'string') {
                if (optionalParamFlag === true && arguments[arguments.length-1].charAt(0) !== '%') {
                    throw new Error("Spec: buildsWith requires parameters preceded with a % to be the final parameters before the optional function");
                } else {
                    for (i=0; i<arguments.length; i++) {
                        requiredArgs = [];
                        optionalArgs = [];
                        if (arguments[i].charAt(0) !== '%') {
                            requiredArgs.push(arguments[i]);
                        } else if (arguments[i].charAt(0) === '%') {
                            optionalArgs.push(arguments[i].slice(1));
                        }
                    }
                    return this;
                }
            } else if (typeof(arguments[arguments.length-1]) === 'function') {
                requiredArgs = [];
                optionalArgs = [];
                for (i=0; i<arguments.length-1; i++) {
                    if (arguments[i].charAt(0) !== '%') {
                        requiredArgs.push(arguments[i]);
                    } else if (arguments[i].charAt(0) === '%') {
                        optionalArgs.push(arguments[i].slice(1));
                    }
                }
                arguments[arguments.length-1]();
                return this;
            } else {
                throw new Error("Spec: buildsWith parameters must be strings except for a function as the optional final parameter");
            }
        };

        this.looksLike = function (p) {
            pattern = p;
        };

        this.respondsTo = function (methodName, methodBody) {
            var m = new Method(methodName, methodBody);
            methods[methodName] = m;
        };

        this.create = function (name) {
            this[name] = function () {
                var i;

                //add attributes
                for(i in attributes) {
                    if(attributes.hasOwnProperty(i)) {
                        attributes[i].addTo(this);
                    }
                }

                //add methods
                for(i in methods) {
                    if(methods.hasOwnProperty(i)) {
                        methods[i].addTo(this);
                    }
                }

                this.toString = pattern;
            };
            return this[name];
        };
    }
    ns.Spec = Spec;
}(window.multigraph.ModelTool));<|endoftext|>


<commit_before>/*
Copyright (c) 2011 Cimaron Shanahan

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

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

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

GraphicsContext3D = (function() {

	function Initializer() {
		//private:
		this.webglCtx = null;
		this.canvas = null;

		this.buffer = null;
		this.context = null;

		this._state = {};
		this._dirty = false;

		this._frame = {};
		this._quality = {};
	}

	var GraphicsContext3D = new jClass('GraphicsContext3D', Initializer);

	//public:

	GraphicsContext3D.GraphicsContext3D = function(webglCtx) {
		var threshold, This;
		this.webglCtx = webglCtx;
		this.canvas = webglCtx.canvas;
		this.context = this.canvas.getContext('2d');

		/*
		//The following attempts to adjust the initial quality setting based
		//the number of pixels in the canvas. This threshold is mostly arbitrary
		//and could stand some more intelligent calculation
		this._quality.startThreshold = 250000;
		if (this.canvas.width * this.canvas.height > this._quality.startThreshold) {
			this._quality.factor = (this.canvas.width * this.canvas.height) / this._quality.startThreshold;
		} else {
			this._quality.factor = 1;
		}
		*/
		this._quality.factor = 1;
		this.setTargetFps(0);

		this._createBuffer();

		//initialize state
		glClearColor(0, 0, 0, 255);
		glClearDepth(1.0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glViewport(0, 0, this.canvas.width, this.canvas.height);

		This = this;
		setInterval(function() { This._redraw(); }, 0);
	};

	//public:

	GraphicsContext3D.activeTexture = function(texture) {
		glActiveTexture(texture);
	};

	GraphicsContext3D.attachShader = function(program, shader) {
		glAttachShader(program, shader);
	};

	GraphicsContext3D.bindAttribLocation = function(program, index, name) {
		glBindAttribLocation(program, index, name);
	};

	GraphicsContext3D.bindBuffer = function(target, buffer) {
		glBindBuffer(target, buffer);
	};

	GraphicsContext3D.bindTexture = function(target, texture) {
		glBindTexture(target, texture);
	};

	GraphicsContext3D.blendFunc = function(sfactor, dfactor) {
		glBlendFunc(sfactor, dfactor);
	};

	GraphicsContext3D.bufferData = function(target, size, data, usage) {
		glBufferData(target, size, data, usage);
	};

	GraphicsContext3D.bufferSubData = function(target, offset, size, data) {
		glBufferSubData(target, offset, size, data);
	};

	GraphicsContext3D.clear = function(mask) {
		this._dirty = true;
		glClear(mask);
	};

	GraphicsContext3D.clearColor = function(red, green, blue, alpha) {
		glClearColor(red, green, blue, alpha);
	};

	GraphicsContext3D.clearDepth = function(depth) {
		glClearDepth(depth);
	};

	GraphicsContext3D.compileShader = function(shader) {
		glCompileShader(shader);
	};

	GraphicsContext3D.createBuffer = function() {
		var buffers = [];
		glGenBuffers(1, buffers);
		return buffers[0][0];
	};

	GraphicsContext3D.createProgram = function() {
		return glCreateProgram();
	};

	GraphicsContext3D.createShader = function(type) {
		return glCreateShader(type);
	};

	GraphicsContext3D.createTexture = function() {
		var textures = [];
		glGenTextures(1, textures);
		return textures[0][0];
	};

	GraphicsContext3D.drawArrays = function(mode, first, count) {
		this._dirty = true;
		glDrawArrays(mode, first, count);
	};

	GraphicsContext3D.drawElements = function(mode, count, type, offset) {
		this._dirty = true;
		glDrawElements(mode, count, type, offset);
	};

	GraphicsContext3D.disable = function(cap) {
		glDisable(cap);
	};

	GraphicsContext3D.enable = function(cap) {
		glEnable(cap);
	};

	GraphicsContext3D.enableVertexAttribArray = function(index) {
		glEnableVertexAttribArray(index);
	};

	GraphicsContext3D.generateMipmap = function(target) {
		//glGenerateMipmap(target);
	};

	GraphicsContext3D.getAttribLocation = function(program, name) {
		return glGetAttribLocation(program, name);
	};

	GraphicsContext3D.getError = function() {
		return glGetError();
	};

	GraphicsContext3D.getProgramParameter = function(program, pname) {
		var params = [];
		glGetProgramiv(program, pname, params);
		return params[0];
	};

	GraphicsContext3D.getShaderInfoLog = function(shader) {
		var length = [], infoLog = [];
		glGetShaderInfoLog(shader, null, length, infoLog);
		return infoLog[0];
	};

	GraphicsContext3D.getShaderParameter = function(shader, pname) {
		var params = [];
		glGetShaderiv(shader, pname, params);
		return params[0];
	};

	GraphicsContext3D.getUniformLocation = function(program, name) {
		return glGetUniformLocation(program, name);
	};

	GraphicsContext3D.linkProgram = function(program) {
		glLinkProgram(program);
	};

	GraphicsContext3D.pixelStorei = function(pname, param) {
		glPixelStorei(pname, param);
	};

	GraphicsContext3D.shaderSource = function(shader, source) {
		glShaderSource(shader, 1, [source], [source.length]);
	};

	GraphicsContext3D.uniform1i = function(location, v0) {
		glUniform1i(location, v0);
	};

	GraphicsContext3D.uniform1f = function(location, v0) {
		glUniform1f(location, v0);
	};

	GraphicsContext3D.uniform3f = function(location, v0, v1, v2) {
		glUniform3f(location, v0, v1, v2);
	};

	GraphicsContext3D.uniform3fv = function(location, value) {
		glUniform3fv(location, 1, value);
	};

	GraphicsContext3D.uniformMatrix3fv = function(location, transpose, value) {
		glUniformMatrix3fv(location, value.length / 9, transpose, value);
	};

	GraphicsContext3D.uniformMatrix4fv = function(location, transpose, value) {
		glUniformMatrix4fv(location, value.length / 16, transpose, value);
	};

	GraphicsContext3D.useProgram = function(program) {
		glUseProgram(program);
	};

	GraphicsContext3D.texImage2D = function(target, level, internalformat, width, height, border, format, type, source) {
		glTexImage2D(target, level, internalformat, width, height, border, format, type, source);
	};

	GraphicsContext3D.texParameteri = function(target, pname, param) {
		glTexParameteri(target, pname, param);
	};

	GraphicsContext3D.vertexAttribPointer = function(idx, size, type, normalized, stride, offset) {
		var pointer = [];
		glVertexAttribPointer(idx, size, type, normalized, stride, offset);
	};

	GraphicsContext3D.viewport = function(x, y, width, height) {
		x = Math.round(x / this._quality.factor);
		y = Math.round(y / this._quality.factor);
		width = Math.round(width / this._quality.factor);
		height = Math.round(height / this._quality.factor);

		glViewport(x, y, width, height);
	};

	GraphicsContext3D.setTargetFps = function(low, high) {
		this._frame.fps = 0;
		this._frame.last = [];
		this._quality.fpsLow = low;
		this._quality.fpsHigh = high ? high : 1.5 * low;
		this._quality.fpsLowThreshold = 0;
		this._quality.fpsHighThreshold = 0;
	};

	//private:

	GraphicsContext3D._createBuffer = function() {
		var ctx, width, height;

		ctx = cnvgl_context.getCurrentContext();

		if (!this.buffer) {
			this.buffer = this.context.createImageData(this.canvas.width, this.canvas.height);
		}
		if (!this._quality.cnv) {
			this._quality.cnv = document.createElement('canvas');
		}

		width = this.canvas.width;
		height = this.canvas.height;

		if (this._quality.factor > 1) {

			width = Math.round(width / this._quality.factor);
			height = Math.round(height / this._quality.factor);
			width = Math.max(width, 1);
			height = Math.max(height, 1);

			this.webglCtx.drawingBufferWidth = width;
			this.webglCtx.drawingBufferHeight = height;

			this._quality.cnv.width = width;
			this._quality.cnv.height = height;
			this._quality.ctx = this._quality.cnv.getContext('2d');
			this.context.mozImageSmoothingEnabled = false;

			this._quality.buffer = this.context.createImageData(width, height);
			ctx.color_buffer = this._quality.buffer.data;

		} else {
			ctx.color_buffer = this.buffer.data;
		}

		ctx.depth_buffer = new Float32Array(width * height);
	};

	GraphicsContext3D._updateFrame = function() {
		var time, dur;
		//update frame counts, fps, etc.
		this._frame.count++;
		time = new Date().getTime();
		this._frame.last.push(time);

		//average over one second period
		dur = time - this._frame.last[0];
		this._frame.fps = 1000 * this._frame.last.length / dur;
		if (dur > 1000) {
			this._frame.last.shift();
		}
	};

	GraphicsContext3D._checkFps = function() {
		var change;

		if (this._frame.fps < this._quality.fpsLow) {
			this._quality.fpsLowThreshold++;
		}

		//must maintain low fps over period of one second
		if (this._quality.fpsLowThreshold > this._frame.fps) {
			this._quality.fpsLowThreshold = 0;
			change = 1 + this._quality.fpsLow / Math.max(this._frame.fps, 1) / 4;
			this._quality.factor *= change;
			this._createBuffer();
		}

		if (this._frame.fps > this._quality.fpsHigh) {
			this._quality.fpsHighThreshold++;
		}

		//must maintain high fps over period of one second
		if (this._quality.fpsHighThreshold > this._frame.fps && this._quality.factor > 1) {
			this._quality.fpsHighThreshold = 0;
			//and even then, only slowly increase the resolution
			this._quality.factor = Math.max(1, this._quality.factor / 1.1);
			this._createBuffer();
		}
	};

	GraphicsContext3D._redraw = function() {

		this._updateFrame();
		if (this._dirty) {
			this._dirty = false;

			if (this._quality.factor > 1) {
				this._quality.ctx.putImageData(this._quality.buffer, 0, 0);
				this.context.drawImage(this._quality.cnv, 0, 0, this.canvas.width, this.canvas.height);
			} else {
				this.context.putImageData(this.buffer, 0, 0);
			}
		}

		this._checkFps();
	};

	return GraphicsContext3D.Constructor;

}());

<commit_msg>Adding support for working with culling<commit_after>/*
Copyright (c) 2011 Cimaron Shanahan

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

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

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

GraphicsContext3D = (function() {

	function Initializer() {
		//private:
		this.webglCtx = null;
		this.canvas = null;

		this.buffer = null;
		this.context = null;

		this._state = {};
		this._dirty = false;

		this._frame = {};
		this._quality = {};
	}

	var GraphicsContext3D = new jClass('GraphicsContext3D', Initializer);

	//public:

	GraphicsContext3D.GraphicsContext3D = function(webglCtx) {
		var threshold, This;
		this.webglCtx = webglCtx;
		this.canvas = webglCtx.canvas;
		this.context = this.canvas.getContext('2d');

		/*
		//The following attempts to adjust the initial quality setting based
		//the number of pixels in the canvas. This threshold is mostly arbitrary
		//and could stand some more intelligent calculation
		this._quality.startThreshold = 250000;
		if (this.canvas.width * this.canvas.height > this._quality.startThreshold) {
			this._quality.factor = (this.canvas.width * this.canvas.height) / this._quality.startThreshold;
		} else {
			this._quality.factor = 1;
		}
		*/
		this._quality.factor = 1;
		this.setTargetFps(0);

		this._createBuffer();

		//initialize state
		glClearColor(0, 0, 0, 255);
		glClearDepth(1.0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		glViewport(0, 0, this.canvas.width, this.canvas.height);

		This = this;
		setInterval(function() { This._redraw(); }, 0);
	};

	//public:

	GraphicsContext3D.activeTexture = function(texture) {
		glActiveTexture(texture);
	};

	GraphicsContext3D.attachShader = function(program, shader) {
		glAttachShader(program, shader);
	};

	GraphicsContext3D.bindAttribLocation = function(program, index, name) {
		glBindAttribLocation(program, index, name);
	};

	GraphicsContext3D.bindBuffer = function(target, buffer) {
		glBindBuffer(target, buffer);
	};

	GraphicsContext3D.bindTexture = function(target, texture) {
		glBindTexture(target, texture);
	};

	GraphicsContext3D.blendFunc = function(sfactor, dfactor) {
		glBlendFunc(sfactor, dfactor);
	};

	GraphicsContext3D.bufferData = function(target, size, data, usage) {
		glBufferData(target, size, data, usage);
	};

	GraphicsContext3D.bufferSubData = function(target, offset, size, data) {
		glBufferSubData(target, offset, size, data);
	};

	GraphicsContext3D.clear = function(mask) {
		this._dirty = true;
		glClear(mask);
	};

	GraphicsContext3D.clearColor = function(red, green, blue, alpha) {
		glClearColor(red, green, blue, alpha);
	};

	GraphicsContext3D.clearDepth = function(depth) {
		glClearDepth(depth);
	};

	GraphicsContext3D.compileShader = function(shader) {
		glCompileShader(shader);
	};

	GraphicsContext3D.createBuffer = function() {
		var buffers = [];
		glGenBuffers(1, buffers);
		return buffers[0][0];
	};

	GraphicsContext3D.createProgram = function() {
		return glCreateProgram();
	};

	GraphicsContext3D.createShader = function(type) {
		return glCreateShader(type);
	};

	GraphicsContext3D.createTexture = function() {
		var textures = [];
		glGenTextures(1, textures);
		return textures[0][0];
	};

	GraphicsContext3D.cullFace = function(mode) {
		glCullFace(mode);
	};

	GraphicsContext3D.drawArrays = function(mode, first, count) {
		this._dirty = true;
		glDrawArrays(mode, first, count);
	};

	GraphicsContext3D.drawElements = function(mode, count, type, offset) {
		this._dirty = true;
		glDrawElements(mode, count, type, offset);
	};

	GraphicsContext3D.disable = function(cap) {
		glDisable(cap);
	};

	GraphicsContext3D.enable = function(cap) {
		glEnable(cap);
	};

	GraphicsContext3D.enableVertexAttribArray = function(index) {
		glEnableVertexAttribArray(index);
	};

	GraphicsContext3D.frontFace = function(mode) {
		glFrontFace(mode);
	};

	GraphicsContext3D.generateMipmap = function(target) {
		//glGenerateMipmap(target);
	};

	GraphicsContext3D.getAttribLocation = function(program, name) {
		return glGetAttribLocation(program, name);
	};

	GraphicsContext3D.getError = function() {
		return glGetError();
	};

	GraphicsContext3D.getProgramParameter = function(program, pname) {
		var params = [];
		glGetProgramiv(program, pname, params);
		return params[0];
	};

	GraphicsContext3D.getShaderInfoLog = function(shader) {
		var length = [], infoLog = [];
		glGetShaderInfoLog(shader, null, length, infoLog);
		return infoLog[0];
	};

	GraphicsContext3D.getShaderParameter = function(shader, pname) {
		var params = [];
		glGetShaderiv(shader, pname, params);
		return params[0];
	};

	GraphicsContext3D.getUniformLocation = function(program, name) {
		return glGetUniformLocation(program, name);
	};

	GraphicsContext3D.linkProgram = function(program) {
		glLinkProgram(program);
	};

	GraphicsContext3D.pixelStorei = function(pname, param) {
		glPixelStorei(pname, param);
	};

	GraphicsContext3D.shaderSource = function(shader, source) {
		glShaderSource(shader, 1, [source], [source.length]);
	};

	GraphicsContext3D.uniform1i = function(location, v0) {
		glUniform1i(location, v0);
	};

	GraphicsContext3D.uniform1f = function(location, v0) {
		glUniform1f(location, v0);
	};

	GraphicsContext3D.uniform3f = function(location, v0, v1, v2) {
		glUniform3f(location, v0, v1, v2);
	};

	GraphicsContext3D.uniform3fv = function(location, value) {
		glUniform3fv(location, 1, value);
	};

	GraphicsContext3D.uniformMatrix3fv = function(location, transpose, value) {
		glUniformMatrix3fv(location, value.length / 9, transpose, value);
	};

	GraphicsContext3D.uniformMatrix4fv = function(location, transpose, value) {
		glUniformMatrix4fv(location, value.length / 16, transpose, value);
	};

	GraphicsContext3D.useProgram = function(program) {
		glUseProgram(program);
	};

	GraphicsContext3D.texImage2D = function(target, level, internalformat, width, height, border, format, type, source) {
		glTexImage2D(target, level, internalformat, width, height, border, format, type, source);
	};

	GraphicsContext3D.texParameteri = function(target, pname, param) {
		glTexParameteri(target, pname, param);
	};

	GraphicsContext3D.vertexAttribPointer = function(idx, size, type, normalized, stride, offset) {
		var pointer = [];
		glVertexAttribPointer(idx, size, type, normalized, stride, offset);
	};

	GraphicsContext3D.viewport = function(x, y, width, height) {
		x = Math.round(x / this._quality.factor);
		y = Math.round(y / this._quality.factor);
		width = Math.round(width / this._quality.factor);
		height = Math.round(height / this._quality.factor);

		glViewport(x, y, width, height);
	};

	GraphicsContext3D.setTargetFps = function(low, high) {
		this._frame.fps = 0;
		this._frame.last = [];
		this._quality.fpsLow = low;
		this._quality.fpsHigh = high ? high : 1.5 * low;
		this._quality.fpsLowThreshold = 0;
		this._quality.fpsHighThreshold = 0;
	};

	//private:

	GraphicsContext3D._createBuffer = function() {
		var ctx, width, height;

		ctx = cnvgl_context.getCurrentContext();

		if (!this.buffer) {
			this.buffer = this.context.createImageData(this.canvas.width, this.canvas.height);
		}
		if (!this._quality.cnv) {
			this._quality.cnv = document.createElement('canvas');
		}

		width = this.canvas.width;
		height = this.canvas.height;

		if (this._quality.factor > 1) {

			width = Math.round(width / this._quality.factor);
			height = Math.round(height / this._quality.factor);
			width = Math.max(width, 1);
			height = Math.max(height, 1);

			this.webglCtx.drawingBufferWidth = width;
			this.webglCtx.drawingBufferHeight = height;

			this._quality.cnv.width = width;
			this._quality.cnv.height = height;
			this._quality.ctx = this._quality.cnv.getContext('2d');
			this.context.mozImageSmoothingEnabled = false;

			this._quality.buffer = this.context.createImageData(width, height);
			ctx.color_buffer = this._quality.buffer.data;

		} else {
			ctx.color_buffer = this.buffer.data;
		}

		ctx.depth_buffer = new Float32Array(width * height);
	};

	GraphicsContext3D._updateFrame = function() {
		var time, dur;
		//update frame counts, fps, etc.
		this._frame.count++;
		time = new Date().getTime();
		this._frame.last.push(time);

		//average over one second period
		dur = time - this._frame.last[0];
		this._frame.fps = 1000 * this._frame.last.length / dur;
		if (dur > 1000) {
			this._frame.last.shift();
		}
	};

	GraphicsContext3D._checkFps = function() {
		var change;

		if (this._frame.fps < this._quality.fpsLow) {
			this._quality.fpsLowThreshold++;
		}

		//must maintain low fps over period of one second
		if (this._quality.fpsLowThreshold > this._frame.fps) {
			this._quality.fpsLowThreshold = 0;
			change = 1 + this._quality.fpsLow / Math.max(this._frame.fps, 1) / 4;
			this._quality.factor *= change;
			this._createBuffer();
		}

		if (this._frame.fps > this._quality.fpsHigh) {
			this._quality.fpsHighThreshold++;
		}

		//must maintain high fps over period of one second
		if (this._quality.fpsHighThreshold > this._frame.fps && this._quality.factor > 1) {
			this._quality.fpsHighThreshold = 0;
			//and even then, only slowly increase the resolution
			this._quality.factor = Math.max(1, this._quality.factor / 1.1);
			this._createBuffer();
		}
	};

	GraphicsContext3D._redraw = function() {

		this._updateFrame();
		if (this._dirty) {
			this._dirty = false;

			if (this._quality.factor > 1) {
				this._quality.ctx.putImageData(this._quality.buffer, 0, 0);
				this.context.drawImage(this._quality.cnv, 0, 0, this.canvas.width, this.canvas.height);
			} else {
				this.context.putImageData(this.buffer, 0, 0);
			}
		}

		this._checkFps();
	};

	return GraphicsContext3D.Constructor;

}());

<|endoftext|>