blob: a500ef73f7cdf295e1b127cde002500df060f122 [file] [log] [blame]
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http:#www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ## t5/core/pageinit
#
# Module that defines functions used for page initialization.
# The initialize function is passed an array of initializers; each initializer is itself
# an array. The first value in the initializer defines the name of the module to invoke.
# The module name may also indicate the function exported by the module, as a suffix following a colon:
# e.g., "my/module:myfunc".
# Any additional values in the initializer are passed to the function. The context of the function (this) is null.
define ["underscore", "./console", "./dom", "./events"],
(_, console, dom, events) ->
pathPrefix = null
# Borrowed from Prototype:
isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'
isIE = !!window.attachEvent && !isOpera
rebuildURL = (path) ->
return path if path.match /^https?:/
# See Tapestry.rebuildURL() for an error about the path not starting with a leading '/'
# We'll assume that doesn't happen.
if !pathPrefix
l = window.location
pathPrefix = "#{l.protocol}//#{l.host}"
return pathPrefix + path
rebuildURLOnIE =
if isIE then rebuildURL else _.identity
addStylesheets = (newStylesheets) ->
return unless newStylesheets
# Figure out which stylesheets are loaded; adjust for IE, and especially, IE 9
# which can report a stylesheet's URL as null (not empty string).
loaded = _.chain(document.styleSheets)
.pluck("href")
.without("")
.without(null)
.map(rebuildURLOnIE)
insertionPoint = _.find(document.styleSheets, (ss) ->
parent = ss.ownerNode || ss.owningElement
parent.rel is "stylesheet t-ajax-insertion-point")
# Most browsers support document.head, but older IE doesn't:
head = document.head or document.getElementsByTagName("head")[0]
_.chain(newStylesheets)
.map((ss) -> { href: rebuildURL(ss.href), media: ss.media })
.reject((ss) -> loaded.contains(ss.href).value())
.each((ss) ->
element = document.createElement "link"
element.setAttribute "type", "text/css"
element.setAttribute "rel", "stylesheet"
element.setAttribute "href", ss.href
if ss.media
element.setAttribute "media", ss.media
if insertionPoint
head.insertBefore element, insertionPoint.ownerNode
else
head.appendChild element
console.debug "Added stylesheet #{ss.href}"
)
return
invokeInitializer = (tracker, qualifiedName, initArguments) ->
[moduleName, functionName] = qualifiedName.split ':'
require [moduleName], (moduleLib) ->
# Some modules export nothing but do some full-page initialization, such as adding
# event handlers to the body.
if not functionName and
initArguments.length is 0 and
not _.isFunction moduleLib
console.debug "Loaded module #{moduleName}"
tracker()
return
fn = if functionName? then moduleLib[functionName] else moduleLib
unless fn?
throw new Error "Could not locate function `#{qualifiedName}'."
if console.debugEnabled
argsString = (JSON.stringify arg for arg in initArguments).join(", ")
console.debug "Invoking #{qualifiedName}(#{argsString})"
fn.apply null, initArguments
tracker()
# Loads all specified libraries in order (this includes the core stack, other stacks, and
# any free-standing libraries). It then executes the initializations. Once all initializations have
# completed (which is usually an asynchronous operation, as initializations may require the loading
# of further modules), then the `data-page-initialized` attribute of the `<body>` element is set to
# 'true'.
#
# This is the main export of the module; other functions are attached as properties.
loadLibrariesAndInitialize = (libraries, inits) ->
console.debug "Loading #{libraries?.length or 0} libraries"
exports.loadLibraries libraries,
-> exports.initialize inits,
->
# At this point, all libraries have been loaded, and all inits should have executed. Unless some of
# the inits triggered Ajax updates (such as a core/ProgressiveDisplay component), then the page should
# be ready to go. We set a flag, mostly used by test suites, to ensure that all is ready.
# Later Ajax requests will cause the data-ajax-active attribute to switch between "true" and "false",
# so some test logic may need to be driven by that instead.
dom.body.attr "data-page-initialized", "true"
for mask in dom.body.find ".pageloading-mask"
mask.remove()
exports = _.extend loadLibrariesAndInitialize,
# Passed a list of initializers, executes each initializer in order. Due to asynchronous loading
# of modules, the exact order in which initializer functions are invoked is not predictable.
initialize: (inits = [], callback) ->
console.debug "Executing #{inits.length} inits"
callbackCountdown = inits.length + 1
# tracker gets invoked once after each require/callback, plus once extra
# (to handle the case where there are no inits). When the count hits zero,
# it invokes the callback (if there is one).
tracker = ->
callbackCountdown--
if callbackCountdown is 0
console.debug "All inits executed"
callback() if callback
# First value in each init is the qualified module name; anything after
# that are arguments to be passed to the identified function. A string
# is the name of a module to load, or function to invoke, that
# takes no parameters.
for init in inits
if _.isString init
invokeInitializer tracker, init, []
else
[qualifiedName, initArguments...] = init
invokeInitializer tracker, qualifiedName, initArguments
tracker()
# Pre-loads a number of libraries in order. When the last library is loaded,
# invokes the callback (with no parameters).
loadLibraries: (libraries, callback) ->
reducer = (callback, library) -> ->
console.debug "Loading library #{library}"
require [library], callback
finalCallback = _.reduceRight libraries, reducer, callback
finalCallback.call null
evalJavaScript: (js) ->
console.debug "Evaluating: #{js}"
eval js
# Triggers the focus event on the field, if the field exist. Focus occurs delayed 1/8th of a
# second, which helps ensure that other initializions on the page are in place.
#
# * fieldId - element id of field to focus on
focus: (fieldId) ->
field = dom fieldId
if field
_.delay (-> field.focus()), 125
# Passed the response from an Ajax request, when the request is successful.
# This is used for any request that attaches partial-page-render data to the main JSON object
# response. If no such data is attached, the callback is simply invoked immediately.
# Otherwise, Tapestry processes the partial-page-render data. This may involve loading some number
# of JavaScript libraries and CSS style sheets, and a number of direct updates to the DOM. After DOM updates,
# the callback is invoked, passed the response (with any Tapestry-specific data removed).
# After the callback is invoked, page initializations occur. This method returns null.
# * response - the Ajax response object
# * callback - invoked after scripts are loaded, but before page initializations occur (may be null)
handlePartialPageRenderResponse: (response, callback) ->
# Capture the partial page response portion of the overall response, and
# then remove it so it doesn't interfere elsewhere.
responseJSON = response.json or {}
partial = responseJSON._tapestry
delete responseJSON._tapestry
# Extreme case: the data has a redirectURL which forces an immediate redirect to the URL.
# No other initialization or callback invocation occurs.
if partial?.redirectURL
window.location.href = partial.redirectURL
return
addStylesheets partial?.stylesheets
# Make sure all libraries are loaded
exports.loadLibraries partial?.libraries, ->
# After libraries are loaded, update each zone:
_(partial?.content).each ([id, content]) ->
console.debug "Updating content for zone #{id}"
zone = dom.wrap id
if zone
zone.trigger events.zone.update, { content }
# Invoke the callback, if present. The callback may do its own content updates.
callback and callback(response)
# Lastly, perform initializations from the partial page render response.
exports.initialize partial?.inits
return