mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-17 19:12:33 -05:00
73bbe598da
Improve image gallery Add How to Copy/Paste section Fix modal close button Add specs for image gallery Fix up i18n Fix render resetting scroll Address code review feedback Ensure afterRender is called
483 lines
17 KiB
CoffeeScript
483 lines
17 KiB
CoffeeScript
module.exports.clone = (obj) ->
|
|
return obj if obj is null or typeof (obj) isnt 'object'
|
|
temp = obj.constructor()
|
|
for key of obj
|
|
temp[key] = module.exports.clone(obj[key])
|
|
temp
|
|
|
|
module.exports.combineAncestralObject = (obj, propertyName) ->
|
|
combined = {}
|
|
while obj?[propertyName]
|
|
for key, value of obj[propertyName]
|
|
continue if combined[key]
|
|
combined[key] = value
|
|
if obj.__proto__
|
|
obj = obj.__proto__
|
|
else
|
|
# IE has no __proto__. TODO: does this even work? At most it doesn't crash.
|
|
obj = Object.getPrototypeOf(obj)
|
|
combined
|
|
|
|
module.exports.normalizeFunc = (func_thing, object) ->
|
|
# func could be a string to a function in this class
|
|
# or a function in its own right
|
|
object ?= {}
|
|
if _.isString(func_thing)
|
|
func = object[func_thing]
|
|
if not func
|
|
console.error "Could not find method #{func_thing} in object", object
|
|
return => null # always return a func, or Mediator will go boom
|
|
func_thing = func
|
|
return func_thing
|
|
|
|
module.exports.objectIdToDate = (objectID) ->
|
|
new Date(parseInt(objectID.toString().slice(0,8), 16)*1000)
|
|
|
|
module.exports.hexToHSL = (hex) ->
|
|
rgbToHsl(hexToR(hex), hexToG(hex), hexToB(hex))
|
|
|
|
hexToR = (h) -> parseInt (cutHex(h)).substring(0, 2), 16
|
|
hexToG = (h) -> parseInt (cutHex(h)).substring(2, 4), 16
|
|
hexToB = (h) -> parseInt (cutHex(h)).substring(4, 6), 16
|
|
cutHex = (h) -> (if (h.charAt(0) is '#') then h.substring(1, 7) else h)
|
|
|
|
module.exports.hslToHex = (hsl) ->
|
|
'#' + (toHex(n) for n in hslToRgb(hsl...)).join('')
|
|
|
|
toHex = (n) ->
|
|
h = Math.floor(n).toString(16)
|
|
h = '0'+h if h.length is 1
|
|
h
|
|
|
|
module.exports.pathToUrl = (path) ->
|
|
base = location.protocol + '//' + location.hostname + (location.port && ":" + location.port)
|
|
base + path
|
|
|
|
module.exports.i18n = (say, target, language=me.get('preferredLanguage', true), fallback='en') ->
|
|
generalResult = null
|
|
fallBackResult = null
|
|
fallForwardResult = null # If a general language isn't available, the first specific one will do.
|
|
fallSidewaysResult = null # If a specific language isn't available, its sibling specific language will do.
|
|
matches = (/\w+/gi).exec(language)
|
|
generalName = matches[0] if matches
|
|
|
|
for localeName, locale of say.i18n
|
|
continue if localeName is '-'
|
|
if target of locale
|
|
result = locale[target]
|
|
else continue
|
|
return result if localeName is language
|
|
generalResult = result if localeName is generalName
|
|
fallBackResult = result if localeName is fallback
|
|
fallForwardResult = result if localeName.indexOf(language) is 0 and not fallForwardResult?
|
|
fallSidewaysResult = result if localeName.indexOf(generalName) is 0 and not fallSidewaysResult?
|
|
|
|
return generalResult if generalResult?
|
|
return fallForwardResult if fallForwardResult?
|
|
return fallSidewaysResult if fallSidewaysResult?
|
|
return fallBackResult if fallBackResult?
|
|
return say[target] if target of say
|
|
null
|
|
|
|
module.exports.getByPath = (target, path) ->
|
|
throw new Error 'Expected an object to match a query against, instead got null' unless target
|
|
pieces = path.split('.')
|
|
obj = target
|
|
for piece in pieces
|
|
return undefined unless piece of obj
|
|
obj = obj[piece]
|
|
obj
|
|
|
|
module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
|
|
|
|
module.exports.round = _.curry (digits, n) ->
|
|
n = +n.toFixed(digits)
|
|
|
|
positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0
|
|
|
|
# f(x) = ax + b
|
|
createLinearFunc = (params) ->
|
|
(x) -> (params.a or 1) * x + (params.b or 0)
|
|
|
|
# f(x) = ax² + bx + c
|
|
createQuadraticFunc = (params) ->
|
|
(x) -> (params.a or 1) * x * x + (params.b or 1) * x + (params.c or 0)
|
|
|
|
# f(x) = a log(b (x + c)) + d
|
|
createLogFunc = (params) ->
|
|
(x) -> if x > 0 then (params.a or 1) * Math.log((params.b or 1) * (x + (params.c or 0))) + (params.d or 0) else 0
|
|
|
|
# f(x) = ax^b + c
|
|
createPowFunc = (params) ->
|
|
(x) -> (params.a or 1) * Math.pow(x, params.b or 1) + (params.c or 0)
|
|
|
|
module.exports.functionCreators =
|
|
linear: positify(createLinearFunc)
|
|
quadratic: positify(createQuadraticFunc)
|
|
logarithmic: positify(createLogFunc)
|
|
pow: positify(createPowFunc)
|
|
|
|
# Call done with true to satisfy the 'until' goal and stop repeating func
|
|
module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) ->
|
|
waitSoFar = 0
|
|
(done = (success) ->
|
|
if (waitSoFar += wait) <= totalWait and not success
|
|
_.delay (-> func done), wait) false
|
|
|
|
module.exports.grayscale = (imageData) ->
|
|
d = imageData.data
|
|
for i in [0..d.length] by 4
|
|
r = d[i]
|
|
g = d[i+1]
|
|
b = d[i+2]
|
|
v = 0.2126*r + 0.7152*g + 0.0722*b
|
|
d[i] = d[i+1] = d[i+2] = v
|
|
imageData
|
|
|
|
# Deep compares l with r, with the exception that undefined values are considered equal to missing values
|
|
# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted
|
|
module.exports.kindaEqual = compare = (l, r) ->
|
|
if _.isObject(l) and _.isObject(r)
|
|
for key in _.union Object.keys(l), Object.keys(r)
|
|
return false unless compare l[key], r[key]
|
|
return true
|
|
else if l is r
|
|
return true
|
|
else
|
|
return false
|
|
|
|
# Return UTC string "YYYYMMDD" for today + offset
|
|
module.exports.getUTCDay = (offset=0) ->
|
|
day = new Date()
|
|
day.setDate(day.getUTCDate() + offset)
|
|
partYear = day.getUTCFullYear()
|
|
partMonth = (day.getUTCMonth() + 1)
|
|
partMonth = "0" + partMonth if partMonth < 10
|
|
partDay = day.getUTCDate()
|
|
partDay = "0" + partDay if partDay < 10
|
|
"#{partYear}#{partMonth}#{partDay}"
|
|
|
|
# Fast, basic way to replace text in an element when you don't need much.
|
|
# http://stackoverflow.com/a/4962398/540620
|
|
if document?.createElement
|
|
dummy = document.createElement 'div'
|
|
dummy.innerHTML = 'text'
|
|
TEXT = if dummy.textContent is 'text' then 'textContent' else 'innerText'
|
|
module.exports.replaceText = (elems, text) ->
|
|
elem[TEXT] = text for elem in elems
|
|
null
|
|
|
|
# Add a stylesheet rule
|
|
# http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript/26230472#26230472
|
|
# Don't use wantonly, or we'll have to implement a simple mechanism for clearing out old rules.
|
|
if document?.createElement
|
|
module.exports.injectCSS = ((doc) ->
|
|
# wrapper for all injected styles and temp el to create them
|
|
wrap = doc.createElement("div")
|
|
temp = doc.createElement("div")
|
|
# rules like "a {color: red}" etc.
|
|
return (cssRules) ->
|
|
# append wrapper to the body on the first call
|
|
unless wrap.id
|
|
wrap.id = "injected-css"
|
|
wrap.style.display = "none"
|
|
doc.body.appendChild wrap
|
|
# <br> for IE: http://goo.gl/vLY4x7
|
|
temp.innerHTML = "<br><style>" + cssRules + "</style>"
|
|
wrap.appendChild temp.children[1]
|
|
return
|
|
)(document)
|
|
|
|
# So that we can stub out userAgent in tests
|
|
module.exports.userAgent = ->
|
|
window.navigator.userAgent
|
|
|
|
module.exports.getQueryVariable = getQueryVariable = (param, defaultValue) ->
|
|
query = document.location.search.substring 1
|
|
pairs = (pair.split('=') for pair in query.split '&')
|
|
for pair in pairs when pair[0] is param
|
|
return {'true': true, 'false': false}[pair[1]] ? decodeURIComponent(pair[1])
|
|
defaultValue
|
|
|
|
module.exports.getSponsoredSubsAmount = getSponsoredSubsAmount = (price=999, subCount=0, personalSub=false) ->
|
|
# 1 100%
|
|
# 2-11 80%
|
|
# 12+ 60%
|
|
# TODO: make this less confusing
|
|
return 0 unless subCount > 0
|
|
offset = if personalSub then 1 else 0
|
|
if subCount <= 1 - offset
|
|
price
|
|
else if subCount <= 11 - offset
|
|
Math.round((1 - offset) * price + (subCount - 1 + offset) * price * 0.8)
|
|
else
|
|
Math.round((1 - offset) * price + 10 * price * 0.8 + (subCount - 11 + offset) * price * 0.6)
|
|
|
|
module.exports.getCourseBundlePrice = getCourseBundlePrice = (coursePrices, seats=20) ->
|
|
totalPricePerSeat = coursePrices.reduce ((a, b) -> a + b), 0
|
|
if coursePrices.length > 2
|
|
pricePerSeat = Math.round(totalPricePerSeat / 2.0)
|
|
else
|
|
pricePerSeat = parseInt(totalPricePerSeat)
|
|
seats * pricePerSeat
|
|
|
|
module.exports.getCoursePraise = getCoursePraise = ->
|
|
praise = [
|
|
{
|
|
quote: "The kids love it."
|
|
source: "Leo Joseph Tran, Athlos Leadership Academy"
|
|
},
|
|
{
|
|
quote: "My students have been using the site for a couple of weeks and they love it."
|
|
source: "Scott Hatfield, Computer Applications Teacher, School Technology Coordinator, Eastside Middle School"
|
|
},
|
|
{
|
|
quote: "Thanks for the captivating site. My eighth graders love it."
|
|
source: "Janet Cook, Ansbach Middle/High School"
|
|
},
|
|
{
|
|
quote: "My students have started working on CodeCombat and love it! I love that they are learning coding and problem solving skills without them even knowing it!!"
|
|
source: "Kristin Huff, Special Education Teacher, Webb City School District"
|
|
},
|
|
{
|
|
quote: "I recently introduced Code Combat to a few of my fifth graders and they are loving it!"
|
|
source: "Shauna Hamman, Fifth Grade Teacher, Four Peaks Elementary School"
|
|
},
|
|
{
|
|
quote: "Overall I think it's a fantastic service. Variables, arrays, loops, all covered in very fun and imaginative ways. Every kid who has tried it is a fan."
|
|
source: "Aibinder Andrew, Technology Teacher"
|
|
},
|
|
{
|
|
quote: "I love what you have created. The kids are so engaged."
|
|
source: "Desmond Smith, 4KS Academy"
|
|
},
|
|
{
|
|
quote: "My students love the website and I hope on having content structured around it in the near future."
|
|
source: "Michael Leonard, Science Teacher, Clearwater Central Catholic High School"
|
|
}
|
|
]
|
|
praise[_.random(0, praise.length - 1)]
|
|
|
|
module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=0, users=0, months=0) ->
|
|
return 0 unless users > 0 and months > 0
|
|
total = price * users * months
|
|
total
|
|
|
|
startsWithVowel = (s) -> s[0] in 'aeiouAEIOU'
|
|
module.exports.filterMarkdownCodeLanguages = (text, language) ->
|
|
return '' unless text
|
|
currentLanguage = language or me.get('aceConfig')?.language or 'python'
|
|
excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'java', 'io', 'html'], currentLanguage
|
|
# Exclude language-specific code blocks like ```python (... code ...)``` for each non-target language.
|
|
codeBlockExclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
|
|
# Exclude language-specific images like ![python - image description](image url) for each non-target language.
|
|
imageExclusionRegex = new RegExp "!\\[(#{excludedLanguages.join('|')}) - .+?\\]\\(.+?\\)\n?", 'gm'
|
|
text = text.replace(codeBlockExclusionRegex, '').replace(imageExclusionRegex, '')
|
|
|
|
commonLanguageReplacements =
|
|
python: [
|
|
['true', 'True'], ['false', 'False'], ['null', 'None'],
|
|
['object', 'dictionary'], ['Object', 'Dictionary'],
|
|
['array', 'list'], ['Array', 'List'],
|
|
]
|
|
lua: [
|
|
['null', 'nil'],
|
|
['object', 'table'], ['Object', 'Table'],
|
|
['array', 'table'], ['Array', 'Table'],
|
|
]
|
|
for [from, to] in commonLanguageReplacements[currentLanguage] ? []
|
|
# Convert JS-specific keywords and types to Python ones, if in simple `code` tags.
|
|
# This won't cover it when it's not in an inline code tag by itself or when it's not in English.
|
|
text = text.replace ///`#{from}`///g, "`#{to}`"
|
|
# Now change "An `dictionary`" to "A `dictionary`", etc.
|
|
if startsWithVowel(from) and not startsWithVowel(to)
|
|
text = text.replace ///(\ a|A)n(\ `#{to}`)///g, "$1$2"
|
|
if not startsWithVowel(from) and startsWithVowel(to)
|
|
text = text.replace ///(\ a|A)(\ `#{to}`)///g, "$1n$2"
|
|
|
|
return text
|
|
|
|
module.exports.aceEditModes = aceEditModes =
|
|
javascript: 'ace/mode/javascript'
|
|
coffeescript: 'ace/mode/coffee'
|
|
python: 'ace/mode/python'
|
|
lua: 'ace/mode/lua'
|
|
java: 'ace/mode/java'
|
|
html: 'ace/mode/html'
|
|
|
|
# These ACEs are used for displaying code snippets statically, like in SpellPaletteEntryView popovers
|
|
# and have short lifespans
|
|
module.exports.initializeACE = (el, codeLanguage) ->
|
|
contents = $(el).text().trim()
|
|
editor = ace.edit el
|
|
editor.setOptions maxLines: Infinity
|
|
editor.setReadOnly true
|
|
editor.setTheme 'ace/theme/textmate'
|
|
editor.setShowPrintMargin false
|
|
editor.setShowFoldWidgets false
|
|
editor.setHighlightActiveLine false
|
|
editor.setHighlightActiveLine false
|
|
editor.setBehavioursEnabled false
|
|
editor.renderer.setShowGutter false
|
|
editor.setValue contents
|
|
editor.clearSelection()
|
|
session = editor.getSession()
|
|
session.setUseWorker false
|
|
session.setMode aceEditModes[codeLanguage]
|
|
session.setWrapLimitRange null
|
|
session.setUseWrapMode true
|
|
session.setNewLineMode 'unix'
|
|
return editor
|
|
|
|
module.exports.capitalLanguages = capitalLanguages =
|
|
'javascript': 'JavaScript'
|
|
'coffeescript': 'CoffeeScript'
|
|
'python': 'Python'
|
|
'java': 'Java'
|
|
'lua': 'Lua'
|
|
'html': 'HTML'
|
|
|
|
module.exports.createLevelNumberMap = (levels) ->
|
|
levelNumberMap = {}
|
|
practiceLevelTotalCount = 0
|
|
practiceLevelCurrentCount = 0
|
|
for level, i in levels
|
|
levelNumber = i - practiceLevelTotalCount + 1
|
|
if level.practice
|
|
levelNumber = i - practiceLevelTotalCount + String.fromCharCode('a'.charCodeAt(0) + practiceLevelCurrentCount)
|
|
practiceLevelTotalCount++
|
|
practiceLevelCurrentCount++
|
|
else
|
|
practiceLevelCurrentCount = 0
|
|
levelNumberMap[level.key] = levelNumber
|
|
levelNumberMap
|
|
|
|
module.exports.findNextLevel = (levels, currentIndex, needsPractice) ->
|
|
# levels = [{practice: true/false, complete: true/false}]
|
|
index = currentIndex
|
|
index++
|
|
if needsPractice
|
|
if levels[currentIndex].practice or index < levels.length and levels[index].practice
|
|
# Needs practice, on practice or next practice, choose next incomplete level
|
|
# May leave earlier practice levels incomplete and reach end of course
|
|
index++ while index < levels.length and levels[index].complete
|
|
else
|
|
# Needs practice, on required, next required, choose first incomplete level of previous practice chain
|
|
index--
|
|
index-- while index >= 0 and not levels[index].practice
|
|
if index >= 0
|
|
index-- while index >= 0 and levels[index].practice
|
|
if index >= 0
|
|
index++
|
|
index++ while index < levels.length and levels[index].practice and levels[index].complete
|
|
if levels[index].practice and not levels[index].complete
|
|
return index
|
|
index = currentIndex + 1
|
|
index++ while index < levels.length and levels[index].complete
|
|
else
|
|
# No practice needed, next required incomplete level
|
|
index++ while index < levels.length and (levels[index].practice or levels[index].complete)
|
|
index
|
|
|
|
module.exports.needsPractice = (playtime=0, threshold=2) ->
|
|
playtime / 60 > threshold
|
|
|
|
module.exports.usStateCodes =
|
|
# https://github.com/mdzhang/us-state-codes
|
|
# generated by js2coffee 2.2.0
|
|
(->
|
|
stateNamesByCode =
|
|
'AL': 'Alabama'
|
|
'AK': 'Alaska'
|
|
'AZ': 'Arizona'
|
|
'AR': 'Arkansas'
|
|
'CA': 'California'
|
|
'CO': 'Colorado'
|
|
'CT': 'Connecticut'
|
|
'DE': 'Delaware'
|
|
'DC': 'District of Columbia'
|
|
'FL': 'Florida'
|
|
'GA': 'Georgia'
|
|
'HI': 'Hawaii'
|
|
'ID': 'Idaho'
|
|
'IL': 'Illinois'
|
|
'IN': 'Indiana'
|
|
'IA': 'Iowa'
|
|
'KS': 'Kansas'
|
|
'KY': 'Kentucky'
|
|
'LA': 'Louisiana'
|
|
'ME': 'Maine'
|
|
'MD': 'Maryland'
|
|
'MA': 'Massachusetts'
|
|
'MI': 'Michigan'
|
|
'MN': 'Minnesota'
|
|
'MS': 'Mississippi'
|
|
'MO': 'Missouri'
|
|
'MT': 'Montana'
|
|
'NE': 'Nebraska'
|
|
'NV': 'Nevada'
|
|
'NH': 'New Hampshire'
|
|
'NJ': 'New Jersey'
|
|
'NM': 'New Mexico'
|
|
'NY': 'New York'
|
|
'NC': 'North Carolina'
|
|
'ND': 'North Dakota'
|
|
'OH': 'Ohio'
|
|
'OK': 'Oklahoma'
|
|
'OR': 'Oregon'
|
|
'PA': 'Pennsylvania'
|
|
'RI': 'Rhode Island'
|
|
'SC': 'South Carolina'
|
|
'SD': 'South Dakota'
|
|
'TN': 'Tennessee'
|
|
'TX': 'Texas'
|
|
'UT': 'Utah'
|
|
'VT': 'Vermont'
|
|
'VA': 'Virginia'
|
|
'WA': 'Washington'
|
|
'WV': 'West Virginia'
|
|
'WI': 'Wisconsin'
|
|
'WY': 'Wyoming'
|
|
stateCodesByName = _.invert(stateNamesByCode)
|
|
# normalizes case and removes invalid characters
|
|
# returns null if can't find sanitized code in the state map
|
|
|
|
sanitizeStateCode = (code) ->
|
|
code = if _.isString(code) then code.trim().toUpperCase().replace(/[^A-Z]/g, '') else null
|
|
if stateNamesByCode[code] then code else null
|
|
|
|
# returns a valid state name else null
|
|
|
|
getStateNameByStateCode = (code) ->
|
|
stateNamesByCode[sanitizeStateCode(code)] or null
|
|
|
|
# normalizes case and removes invalid characters
|
|
# returns null if can't find sanitized name in the state map
|
|
|
|
sanitizeStateName = (name) ->
|
|
if !_.isString(name)
|
|
return null
|
|
# bad whitespace remains bad whitespace e.g. "O hi o" is not valid
|
|
name = name.trim().toLowerCase().replace(/[^a-z\s]/g, '').replace(/\s+/g, ' ')
|
|
tokens = name.split(/\s+/)
|
|
tokens = _.map(tokens, (token) ->
|
|
token.charAt(0).toUpperCase() + token.slice(1)
|
|
)
|
|
# account for District of Columbia
|
|
if tokens.length > 2
|
|
tokens[1] = tokens[1].toLowerCase()
|
|
name = tokens.join(' ')
|
|
if stateCodesByName[name] then name else null
|
|
|
|
# returns a valid state code else null
|
|
|
|
getStateCodeByStateName = (name) ->
|
|
stateCodesByName[sanitizeStateName(name)] or null
|
|
|
|
return {
|
|
sanitizeStateCode: sanitizeStateCode
|
|
getStateNameByStateCode: getStateNameByStateCode
|
|
sanitizeStateName: sanitizeStateName
|
|
getStateCodeByStateName: getStateCodeByStateName
|
|
}
|
|
)()
|