1
0
Fork 0
mirror of https://gitlab.com/Oreolek/salet.git synced 2024-07-07 01:04:25 +03:00
salet/lib/view.coffee

278 lines
9.2 KiB
CoffeeScript
Raw Normal View History

markdown = require('./markdown.coffee')
2016-01-24 08:33:39 +02:00
###
Salet interface configuration.
In a typical MVC structure, this is the View.
Only it knows about the DOM structure.
Other modules just use its API or prepare the HTML for insertion.
2016-01-24 09:53:03 +02:00
You don't need to call this module from the game directly.
The abstraction goal here is to provide the author with a freedom to style his
game as he wants to. The save and erase buttons are not necessary buttons,
but they could be something else entirely. (That's why IDs are hardcoded.)
2016-01-24 08:33:39 +02:00
###
assert = (msg, assertion) -> console.assert assertion, msg
way_to = (content, ref) ->
return "<a href='#{ref}' class='way'>#{content}</a>"
2016-02-01 10:51:48 +02:00
addClass = (element, className) ->
if (element.classList)
element.classList.add(className)
else
element.className += ' ' + className
2016-01-24 08:33:39 +02:00
class SaletView
init: (salet) ->
2016-01-25 18:05:32 +02:00
$("#content, #ways").on("click", "a", (event) ->
2016-01-24 08:33:39 +02:00
event.preventDefault()
a = $(this)
href = a.attr('href')
if (href.match(salet.linkRe))
if (a.hasClass("once") || href.match(/[?&]once[=&]?/))
salet.view.clearLinks(href)
salet.processClick(href)
2016-01-24 08:33:39 +02:00
)
$("#inventory").on("click", "a", (event) ->
event.preventDefault()
2016-01-25 18:05:32 +02:00
alert("Not done yet")
2016-01-24 08:33:39 +02:00
)
$("#load").on("click", "a", (event) ->
window.location.reload()
)
if (@hasLocalStorage())
2016-02-01 15:23:20 +02:00
$("#erase").click((event) ->
event.preventDefault()
return salet.eraseSave()
)
$("#save").click((event) ->
event.preventDefault()
return salet.saveGame()
)
2016-01-24 08:33:39 +02:00
disableSaving: () ->
$("#save").addClass('disabled')
enableSaving: () ->
$("#save").removeClass('disabled')
enableErasing: () ->
$("#erase").removeClass('disabled')
disableErasing: () ->
$("#erase").addClass('disabled')
enableLoading: () ->
$("#load").removeClass('disabled')
disableLoading: () ->
$("#load").addClass('disabled')
# Scrolls the top of the screen to the specified point
scrollTopTo: (value) ->
$('html,body').animate({scrollTop: value}, 500)
# Scrolls the bottom of the screen to the specified point
scrollBottomTo: (value) ->
scrollTopTo(value - $(window).height())
# Scrolls all the way to the bottom of the screen
scrollToBottom: () ->
scrollTopTo($('html').height() - $(window).height());
###
Removes all content from the page, clearing the main content area.
If an elementSelector is given, then only that selector will be
cleared. Note that all content from the cleared element is removed,
but the element itself remains, ready to be filled again using @write.
###
clearContent: (elementSelector = "#content") ->
if (elementSelector == "#content") # empty the intro with the content
2016-01-24 09:53:03 +02:00
document.getElementById("intro").innerHTML = ""
2016-01-24 08:33:39 +02:00
document.querySelector(elementSelector).innerHTML = ""
2016-02-01 10:51:48 +02:00
prepareContent: (content) ->
2016-01-24 08:33:39 +02:00
if typeof content == "function"
content = content()
if content instanceof jQuery
content = content[0].outerHTML
2016-02-01 10:51:48 +02:00
return content.toString()
# Write content to current room
write: (content, elementSelector = "#current-room") ->
if content == ""
return
content = @prepareContent(content)
block = document.querySelector(elementSelector)
2016-01-24 08:33:39 +02:00
if block
block.innerHTML = block.innerHTML + markdown(content)
else
# most likely this is the starting room
block = document.getElementById("content")
block.innerHTML = content
2016-01-24 08:33:39 +02:00
2016-02-01 10:51:48 +02:00
# Replaces the text in the given block with the given text.
# !! Does not call markdown on the provided text. !!
replace: (content, elementSelector) ->
if content == ""
return
content = @prepareContent(content)
block = document.querySelector(elementSelector)
block.innerHTML = content
2016-01-24 08:33:39 +02:00
###
Turns any links that target the given href into plain
text. This can be used to remove action options when an action
is no longer available. It is used automatically when you give
a link the 'once' class.
###
clearLinks: (code) ->
for a in $("#content").find("a[href='" + code + "']")
html = a.innerHTML
a = $(a)
a.replaceWith($("<span>").addClass("ex_link").html(html))
return true
2016-01-24 08:33:39 +02:00
###
Given a list of situation ids, this outputs a standard option
block with the situation choices in the given order.
The contents of each choice will be a link to the situation,
the text of the link will be given by the situation's
outputText property. Note that the canChoose function is
called, and if it returns false, then the text will appear, but
the link will not be clickable.
Although canChoose is honored, canView and displayOrder are
not. If you need to honor these, you should either do so
manually, ot else use the `getSituationIdChoices` method to
return an ordered list of valid viewable situation ids.
###
writeChoices: (salet, listOfIds) ->
if (not listOfIds? or listOfIds.length == 0)
2016-01-24 08:33:39 +02:00
return
currentRoom = salet.getCurrentRoom();
2016-01-24 08:33:39 +02:00
$options = $("<ul>").addClass("options");
for roomId in listOfIds
room = salet.rooms[roomId]
assert(room, "unknown_situation".l({id:roomId}))
if (room == currentRoom)
2016-01-24 08:33:39 +02:00
continue
optionText = room.optionText.fcall(salet, currentRoom)
2016-01-24 08:33:39 +02:00
if (!optionText)
optionText = "choice".l({number:i+1})
$option = $("<li>")
$a = $("<span>")
if (room.canChoose.fcall(this, salet, currentRoom))
$a = $("<a>").attr({href: roomId})
2016-01-24 08:33:39 +02:00
$a.html(optionText)
$option.html($a)
$options.append($option)
@write($options)
# Marks all links as old. This gets called in a `processLink` function.
mark_all_links_old: () ->
$('.new').removeClass('new')
# Removes links and transient sections.
# Arguments:
# interactive - if we're working in interactive mode (or we're loading a save)
removeTransient: (interactive = false) ->
for a in $('#content').find('a')
a = $(a)
2016-01-24 08:33:39 +02:00
if (a.hasClass('sticky') || a.attr("href").match(/[?&]sticky[=&]?/))
return;
a.replaceWith($("<span>").addClass("ex_link").html(a.html()))
contentToHide = $('#content .transient, #content ul.options')
contentToHide.add($("#content a").filter(() ->
return $(this).attr("href").match(/[?&]transient[=&]?/)
))
if (interactive)
contentToHide.animate({opacity: 0}, 350).
slideUp(500, () ->
$(this).remove()
)
else
contentToHide.remove()
# At last, we scroll the view so that .new objects are in view.
endOutputTransaction: () =>
if !@interactive
return; # We're loading a save; do nothing at all.
$new = $('.new')
viewHeight = $(window).height()
newTop = newBottom = newHeight = optionHeight = 0
if $new.length == 0
return; # Somehow, there's nothing new.
newTop = $new.first().offset().top
newBottom = $new.last().offset().top + $new.last().height()
newHeight = newBottom - newTop
# We take the options list into account, because we don't want the new
# content to scroll offscreen when the list disappears. So we calculate
# scroll points as though the option list was already gone.
if ($('.options').not('.new').length)
optionHeight = $('.options').not('new').height()
if (newHeight > (viewHeight - optionHeight - 50))
# The new content is too long for our viewport, so we scroll the
# top of the new content to roughly 75% of the way up the viewport's
# height.
scrollTopTo(newTop-(viewHeight*0.25) - optionHeight);
else
if (newTop > $('body').height() - viewHeight)
# If we scroll right to the bottom, the new content will be in
# view. So we do that.
scrollToBottom();
else
# Our new content is too far up the page. So we scroll to place
# it somewhere near the bottom.
scrollBottomTo(newBottom+100 - optionHeight);
2016-01-24 09:53:03 +02:00
# Feature detection
hasLocalStorage: () ->
2016-02-01 15:23:20 +02:00
return window.localStorage?
2016-01-24 09:53:03 +02:00
updateWays: (salet, ways, name) ->
content = ""
distances = []
if ways then for way in ways
if salet.rooms[way]?
title = salet.rooms[way].title.fcall(this, name)
content += way_to(title, way)
distances.push({
key: way
distance: salet.rooms[way].distance
})
else
document.querySelector(".ways h2").style.display = "none"
document.getElementById("ways").innerHTML = content
min = Infinity
min_key = []
for node in distances
if node.distance < min
min = node.distance
min_key = [node.key]
if node.distance == min
min_key.push(node.key)
if min < Infinity
for node in min_key
waylink = document.getElementById("waylink-#{node}")
if waylink
addClass(waylink, "destination")
2016-02-01 15:39:16 +02:00
pictureTag: (picture) ->
extension = picture.substr((~-picture.lastIndexOf(".") >>> 0) + 2)
if (extension == "webm")
return """
<video src="#{picture}" controls>
Your browser does not support the video tag for some reason.
You won't be able to view this video in this browser.
</video>
"""
return "<img class='img-responsive' src='#{picture}' alt='Room illustration'>"
2016-02-01 15:39:16 +02:00
cycleLink: (content) ->
return "<a href='./_replacer_cyclewriter' class='cycle' id='cyclewriter'>#{content}</a>"
2016-01-25 18:05:32 +02:00
module.exports = SaletView