diff --git a/include/class.format.php b/include/class.format.php index ac9f94c3b42a9e5f1af87d196d0a2e24ac4fdacb..861f8796dd0768948eb14f321e810b9a69b0ae8f 100644 --- a/include/class.format.php +++ b/include/class.format.php @@ -353,6 +353,7 @@ class Format { .'&auth='.$token; // ALL link targets open in a new tab $a['target'] = '_blank'; + $a['class'] = 'no-pjax'; } // Images which are external are rewritten to <div // data-src='url...'/> diff --git a/include/class.osticket.php b/include/class.osticket.php index abd563839d6be9910ae4446f4aaf71694b1dc96b..ce5d6354498dbb022ec66213f756eb49ffb1c69b 100644 --- a/include/class.osticket.php +++ b/include/class.osticket.php @@ -42,6 +42,7 @@ class osTicket { var $title; //Custom title. html > head > title. var $headers; + var $pjax_extra; var $config; var $session; @@ -163,13 +164,17 @@ class osTicket { return $replacer->replaceVars($input); } - function addExtraHeader($header) { + function addExtraHeader($header, $pjax_script=false) { $this->headers[md5($header)] = $header; + $this->pjax_extra[md5($header)] = $pjax_script; } function getExtraHeaders() { return $this->headers; } + function getExtraPjax() { + return $this->pjax_extra; + } function setPageTitle($title) { $this->title = $title; diff --git a/include/class.thread.php b/include/class.thread.php index 5aed962fd07bf8f2197244825d719a9969b31ec4..05d8f67b45e17a05112193c3b42a5a918ad13579 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -602,7 +602,7 @@ Class ThreadEntry { if($attachment['size']) $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size'])); - $str.=sprintf('<a class="Icon file" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', + $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s %s', $file, $attachment['attach_id'], $hash, $target, Format::htmlchars($attachment['name']), $size, $separator); } diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php index 5cb94ea3d817be39355871fb783369dedd009cb9..d112e157a52b98d0832a68f69f77e24751ae1f5f 100644 --- a/include/staff/footer.inc.php +++ b/include/staff/footer.inc.php @@ -1,3 +1,4 @@ +<?php if (!isset($_SERVER['HTTP_X_PJAX'])) { ?> </div> <div id="footer"> Copyright © 2006-<?php echo date('Y'); ?> <?php echo (string) $ost->company ?: 'osTicket.com'; ?> All Rights Reserved. @@ -14,11 +15,21 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?> </div> <div id="overlay"></div> <div id="loading"> - <h4>Please Wait!</h4> - <p>Please wait... it will take a second!</p> + <i class="icon-spinner icon-spin icon-3x pull-left icon-light"></i> + <h1>Loading ...</h1> </div> <div class="dialog" style="display:none;width:650px;" id="popup"> <div class="body"></div> </div> +<script type="text/javascript"> +if ($.support.pjax) { + $(document).on('click', 'a', function(event) { + if (!$(this).hasClass('no-pjax')) + $.pjax.click(event, {container: $('#content'), timeout: 2000}) + }) +} +</script> </body> </html> +<?php } # endif X_PJAX ?> + diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php index d2a3e43b82277bb13eb38718e06bfd84340457f9..2b0870d27388842c36b1d4751ef7451747cc35c7 100644 --- a/include/staff/header.inc.php +++ b/include/staff/header.inc.php @@ -1,3 +1,4 @@ +<?php if (!isset($_SERVER['HTTP_X_PJAX'])) { ?> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> @@ -13,6 +14,7 @@ <![endif]--> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script> <script type="text/javascript" src="../js/jquery.multifile.js"></script> <script type="text/javascript" src="./js/tips.js"></script> <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script> @@ -53,61 +55,26 @@ <p id="info">Welcome, <strong><?php echo $thisstaff->getFirstName(); ?></strong> <?php if($thisstaff->isAdmin() && !defined('ADMINPAGE')) { ?> - | <a href="admin.php">Admin Panel</a> + | <a href="admin.php" class="no-pjax">Admin Panel</a> <?php }else{ ?> - | <a href="index.php">Staff Panel</a> + | <a href="index.php" class="no-pjax">Staff Panel</a> <?php } ?> | <a href="profile.php">My Preferences</a> - | <a href="logout.php?auth=<?php echo $ost->getLinkToken(); ?>">Log Out</a> + | <a href="logout.php?auth=<?php echo $ost->getLinkToken(); ?>" class="no-pjax">Log Out</a> </p> </div> <ul id="nav"> - <?php - if(($tabs=$nav->getTabs()) && is_array($tabs)){ - foreach($tabs as $name =>$tab) { - echo sprintf('<li class="%s"><a href="%s">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']); - if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){ - echo "<ul>\n"; - foreach($subnav as $k => $item) { - if (!($id=$item['id'])) - $id="nav$k"; - - echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>', - $item['iconclass'], $item['href'], $item['title'], $id, $item['desc']); - } - echo "\n</ul>\n"; - } - echo "\n</li>\n"; - } - } ?> +<?php include STAFFINC_DIR . "templates/navigation.tmpl.php"; ?> </ul> <ul id="sub_nav"> - <?php - if(($subnav=$nav->getSubMenu()) && is_array($subnav)){ - $activeMenu=$nav->getActiveMenu(); - if($activeMenu>0 && !isset($subnav[$activeMenu-1])) - $activeMenu=0; - foreach($subnav as $k=> $item) { - if($item['droponly']) continue; - $class=$item['iconclass']; - if ($activeMenu && $k+1==$activeMenu - or (!$activeMenu - && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false - or ($item['urls'] - && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls']) - ) - ))) - $class="$class active"; - if (!($id=$item['id'])) - $id="subnav$k"; - - echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>', - $class, $item['href'], $item['title'], $id, $item['desc']); - } - } - ?> +<?php include STAFFINC_DIR . "templates/sub-navigation.tmpl.php"; ?> </ul> <div id="content"> +<?php } elseif ($pjax = $ost->getExtraPjax()) { # endif X_PJAX ?> + <script type="text/javascript"> + <?php foreach (array_filter($pjax) as $s) echo $s.";"; ?> + </script> +<?php } # endif X_PJAX ?> <?php if($errors['err']) { ?> <div id="msg_error"><?php echo $errors['err']; ?></div> <?php }elseif($msg) { ?> diff --git a/include/staff/templates/navigation.tmpl.php b/include/staff/templates/navigation.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..66f5564c122abd62710171e84e38d9c3e86aa536 --- /dev/null +++ b/include/staff/templates/navigation.tmpl.php @@ -0,0 +1,22 @@ +<?php +if(($tabs=$nav->getTabs()) && is_array($tabs)){ + foreach($tabs as $name =>$tab) { + echo sprintf('<li class="%s"><a href="%s" class="no-pjax">%s</a>',$tab['active']?'active':'inactive',$tab['href'],$tab['desc']); + if(!$tab['active'] && ($subnav=$nav->getSubMenu($name))){ + echo "<ul>\n"; + foreach($subnav as $k => $item) { + if (!($id=$item['id'])) + $id="nav$k"; + + echo sprintf( + '<li><a class="%s %s" href="%s" title="%s" id="%s">%s</a></li>', + $item['iconclass'], + $tab['active'] ? '' : 'no-pjax', + $item['href'], $item['title'], + $id, $item['desc']); + } + echo "\n</ul>\n"; + } + echo "\n</li>\n"; + } +} ?> diff --git a/include/staff/templates/sub-navigation.tmpl.php b/include/staff/templates/sub-navigation.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..ebc6fa20440fefaeb9705814ae50c1c74d311817 --- /dev/null +++ b/include/staff/templates/sub-navigation.tmpl.php @@ -0,0 +1,25 @@ +<?php +if(($subnav=$nav->getSubMenu()) && is_array($subnav)){ + $activeMenu=$nav->getActiveMenu(); + if($activeMenu>0 && !isset($subnav[$activeMenu-1])) + $activeMenu=0; + foreach($subnav as $k=> $item) { + if($item['droponly']) continue; + $class=$item['iconclass']; + if ($activeMenu && $k+1==$activeMenu + or (!$activeMenu + && (strpos(strtoupper($item['href']),strtoupper(basename($_SERVER['SCRIPT_NAME']))) !== false + or ($item['urls'] + && in_array(basename($_SERVER['SCRIPT_NAME']),$item['urls']) + ) + ))) + $class="$class active"; + if (!($id=$item['id'])) + $id="subnav$k"; + + echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>', + $class, $item['href'], $item['title'], $id, $item['desc']); + } +} +?> + diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 621365fc4cd85b0c06cab4128c9f10ea1331affe..d155e040cfb7446f5c546cedc5a3cdee77b58e55 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -982,7 +982,6 @@ $tcount+= $ticket->getNumNotes(); </form> <div class="clear"></div> </div> -<script type="text/javascript" src="js/ticket.js"></script> <script type="text/javascript"> $(function() { $(document).on('click', 'a.change-user', function(e) { diff --git a/js/jquery.pjax.js b/js/jquery.pjax.js new file mode 100644 index 0000000000000000000000000000000000000000..e2f958716ed658d8a1e6ee0563d08cf7a7820b8c --- /dev/null +++ b/js/jquery.pjax.js @@ -0,0 +1,839 @@ +// jquery.pjax.js +// copyright chris wanstrath +// https://github.com/defunkt/jquery-pjax + +(function($){ + +// When called on a container with a selector, fetches the href with +// ajax into the container or with the data-pjax attribute on the link +// itself. +// +// Tries to make sure the back button and ctrl+click work the way +// you'd expect. +// +// Exported as $.fn.pjax +// +// Accepts a jQuery ajax options object that may include these +// pjax specific options: +// +// +// container - Where to stick the response body. Usually a String selector. +// $(container).html(xhr.responseBody) +// (default: current jquery context) +// push - Whether to pushState the URL. Defaults to true (of course). +// replace - Want to use replaceState instead? That's cool. +// +// For convenience the second parameter can be either the container or +// the options object. +// +// Returns the jQuery object +function fnPjax(selector, container, options) { + var context = this + return this.on('click.pjax', selector, function(event) { + var opts = $.extend({}, optionsFor(container, options)) + if (!opts.container) + opts.container = $(this).attr('data-pjax') || context + handleClick(event, opts) + }) +} + +// Public: pjax on click handler +// +// Exported as $.pjax.click. +// +// event - "click" jQuery.Event +// options - pjax options +// +// Examples +// +// $(document).on('click', 'a', $.pjax.click) +// // is the same as +// $(document).pjax('a') +// +// $(document).on('click', 'a', function(event) { +// var container = $(this).closest('[data-pjax-container]') +// $.pjax.click(event, container) +// }) +// +// Returns nothing. +function handleClick(event, container, options) { + options = optionsFor(container, options) + + var link = event.currentTarget + + if (link.tagName.toUpperCase() !== 'A') + throw "$.fn.pjax or $.pjax.click requires an anchor element" + + // Middle click, cmd click, and ctrl click should open + // links in a new tab as normal. + if ( event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey ) + return + + // Ignore cross origin links + if ( location.protocol !== link.protocol || location.hostname !== link.hostname ) + return + + // Ignore anchors on the same page + if (link.hash && link.href.replace(link.hash, '') === + location.href.replace(location.hash, '')) + return + + // Ignore empty anchor "foo.html#" + if (link.href === location.href + '#') + return + + var defaults = { + url: link.href, + container: $(link).attr('data-pjax'), + target: link + } + + var opts = $.extend({}, defaults, options) + var clickEvent = $.Event('pjax:click') + $(link).trigger(clickEvent, [opts]) + + if (!clickEvent.isDefaultPrevented()) { + pjax(opts) + event.preventDefault() + $(link).trigger('pjax:clicked', [opts]) + } +} + +// Public: pjax on form submit handler +// +// Exported as $.pjax.submit +// +// event - "click" jQuery.Event +// options - pjax options +// +// Examples +// +// $(document).on('submit', 'form', function(event) { +// var container = $(this).closest('[data-pjax-container]') +// $.pjax.submit(event, container) +// }) +// +// Returns nothing. +function handleSubmit(event, container, options) { + options = optionsFor(container, options) + + var form = event.currentTarget + + if (form.tagName.toUpperCase() !== 'FORM') + throw "$.pjax.submit requires a form element" + + var defaults = { + type: form.method.toUpperCase(), + url: form.action, + data: $(form).serializeArray(), + container: $(form).attr('data-pjax'), + target: form + } + + pjax($.extend({}, defaults, options)) + + event.preventDefault() +} + +// Loads a URL with ajax, puts the response body inside a container, +// then pushState()'s the loaded URL. +// +// Works just like $.ajax in that it accepts a jQuery ajax +// settings object (with keys like url, type, data, etc). +// +// Accepts these extra keys: +// +// container - Where to stick the response body. +// $(container).html(xhr.responseBody) +// push - Whether to pushState the URL. Defaults to true (of course). +// replace - Want to use replaceState instead? That's cool. +// +// Use it just like $.ajax: +// +// var xhr = $.pjax({ url: this.href, container: '#main' }) +// console.log( xhr.readyState ) +// +// Returns whatever $.ajax returns. +function pjax(options) { + options = $.extend(true, {}, $.ajaxSettings, pjax.defaults, options) + + if ($.isFunction(options.url)) { + options.url = options.url() + } + + var target = options.target + + var hash = parseURL(options.url).hash + + var context = options.context = findContainerFor(options.container) + + // We want the browser to maintain two separate internal caches: one + // for pjax'd partial page loads and one for normal page loads. + // Without adding this secret parameter, some browsers will often + // confuse the two. + if (!options.data) options.data = {} + options.data._pjax = context.selector + + function fire(type, args) { + var event = $.Event(type, { relatedTarget: target }) + context.trigger(event, args) + return !event.isDefaultPrevented() + } + + var timeoutTimer + + options.beforeSend = function(xhr, settings) { + // No timeout for non-GET requests + // Its not safe to request the resource again with a fallback method. + if (settings.type !== 'GET') { + settings.timeout = 0 + } + + xhr.setRequestHeader('X-PJAX', 'true') + xhr.setRequestHeader('X-PJAX-Container', context.selector) + + if (!fire('pjax:beforeSend', [xhr, settings])) + return false + + if (settings.timeout > 0) { + timeoutTimer = setTimeout(function() { + if (fire('pjax:timeout', [xhr, options])) + xhr.abort('timeout') + }, settings.timeout) + + // Clear timeout setting so jquerys internal timeout isn't invoked + settings.timeout = 0 + } + + options.requestUrl = parseURL(settings.url).href + } + + options.complete = function(xhr, textStatus) { + if (timeoutTimer) + clearTimeout(timeoutTimer) + + fire('pjax:complete', [xhr, textStatus, options]) + + fire('pjax:end', [xhr, options]) + } + + options.error = function(xhr, textStatus, errorThrown) { + var container = extractContainer("", xhr, options) + + var allowed = fire('pjax:error', [xhr, textStatus, errorThrown, options]) + if (options.type == 'GET' && textStatus !== 'abort' && allowed) { + locationReplace(container.url) + } + } + + options.success = function(data, status, xhr) { + // If $.pjax.defaults.version is a function, invoke it first. + // Otherwise it can be a static string. + var currentVersion = (typeof $.pjax.defaults.version === 'function') ? + $.pjax.defaults.version() : + $.pjax.defaults.version + + var latestVersion = xhr.getResponseHeader('X-PJAX-Version') + + var container = extractContainer(data, xhr, options) + + // If there is a layout version mismatch, hard load the new url + if (currentVersion && latestVersion && currentVersion !== latestVersion) { + locationReplace(container.url) + return + } + + // If the new response is missing a body, hard load the page + if (!container.contents) { + locationReplace(container.url) + return + } + + pjax.state = { + id: options.id || uniqueId(), + url: container.url, + title: container.title, + container: context.selector, + fragment: options.fragment, + timeout: options.timeout + } + + if (options.push || options.replace) { + window.history.replaceState(pjax.state, container.title, container.url) + } + + // Clear out any focused controls before inserting new page contents. + document.activeElement.blur() + + if (container.title) document.title = container.title + context.html(container.contents) + + // FF bug: Won't autofocus fields that are inserted via JS. + // This behavior is incorrect. So if theres no current focus, autofocus + // the last field. + // + // http://www.w3.org/html/wg/drafts/html/master/forms.html + var autofocusEl = context.find('input[autofocus], textarea[autofocus]').last()[0] + if (autofocusEl && document.activeElement !== autofocusEl) { + autofocusEl.focus(); + } + + executeScriptTags(container.scripts) + + // Scroll to top by default + if (typeof options.scrollTo === 'number') + $(window).scrollTop(options.scrollTo) + + // If the URL has a hash in it, make sure the browser + // knows to navigate to the hash. + if ( hash !== '' ) { + // Avoid using simple hash set here. Will add another history + // entry. Replace the url with replaceState and scroll to target + // by hand. + // + // window.location.hash = hash + var url = parseURL(container.url) + url.hash = hash + + pjax.state.url = url.href + window.history.replaceState(pjax.state, container.title, url.href) + + var target = $(url.hash) + if (target.length) $(window).scrollTop(target.offset().top) + } + + fire('pjax:success', [data, status, xhr, options]) + } + + + // Initialize pjax.state for the initial page load. Assume we're + // using the container and options of the link we're loading for the + // back button to the initial page. This ensures good back button + // behavior. + if (!pjax.state) { + pjax.state = { + id: uniqueId(), + url: window.location.href, + title: document.title, + container: context.selector, + fragment: options.fragment, + timeout: options.timeout + } + window.history.replaceState(pjax.state, document.title) + } + + // Cancel the current request if we're already pjaxing + var xhr = pjax.xhr + if ( xhr && xhr.readyState < 4) { + xhr.onreadystatechange = $.noop + xhr.abort() + } + + pjax.options = options + var xhr = pjax.xhr = $.ajax(options) + + if (xhr.readyState > 0) { + if (options.push && !options.replace) { + // Cache current container element before replacing it + cachePush(pjax.state.id, context.clone().contents()) + + window.history.pushState(null, "", stripPjaxParam(options.requestUrl)) + } + + fire('pjax:start', [xhr, options]) + fire('pjax:send', [xhr, options]) + } + + return pjax.xhr +} + +// Public: Reload current page with pjax. +// +// Returns whatever $.pjax returns. +function pjaxReload(container, options) { + var defaults = { + url: window.location.href, + push: false, + replace: true, + scrollTo: false + } + + return pjax($.extend(defaults, optionsFor(container, options))) +} + +// Internal: Hard replace current state with url. +// +// Work for around WebKit +// https://bugs.webkit.org/show_bug.cgi?id=93506 +// +// Returns nothing. +function locationReplace(url) { + window.history.replaceState(null, "", "#") + window.location.replace(url) +} + + +var initialPop = true +var initialURL = window.location.href +var initialState = window.history.state + +// Initialize $.pjax.state if possible +// Happens when reloading a page and coming forward from a different +// session history. +if (initialState && initialState.container) { + pjax.state = initialState +} + +// Non-webkit browsers don't fire an initial popstate event +if ('state' in window.history) { + initialPop = false +} + +// popstate handler takes care of the back and forward buttons +// +// You probably shouldn't use pjax on pages with other pushState +// stuff yet. +function onPjaxPopstate(event) { + var state = event.state + + if (state && state.container) { + // When coming forward from a separate history session, will get an + // initial pop with a state we are already at. Skip reloading the current + // page. + if (initialPop && initialURL == state.url) return + + // If popping back to the same state, just skip. + // Could be clicking back from hashchange rather than a pushState. + if (pjax.state && pjax.state.id === state.id) return + + var container = $(state.container) + if (container.length) { + var direction, contents = cacheMapping[state.id] + + if (pjax.state) { + // Since state ids always increase, we can deduce the history + // direction from the previous state. + direction = pjax.state.id < state.id ? 'forward' : 'back' + + // Cache current container before replacement and inform the + // cache which direction the history shifted. + cachePop(direction, pjax.state.id, container.clone().contents()) + } + + var popstateEvent = $.Event('pjax:popstate', { + state: state, + direction: direction + }) + container.trigger(popstateEvent) + + var options = { + id: state.id, + url: state.url, + container: container, + push: false, + fragment: state.fragment, + timeout: state.timeout, + scrollTo: false + } + + if (contents) { + container.trigger('pjax:start', [null, options]) + + if (state.title) document.title = state.title + container.html(contents) + pjax.state = state + + container.trigger('pjax:end', [null, options]) + } else { + pjax(options) + } + + // Force reflow/relayout before the browser tries to restore the + // scroll position. + container[0].offsetHeight + } else { + locationReplace(location.href) + } + } + initialPop = false +} + +// Fallback version of main pjax function for browsers that don't +// support pushState. +// +// Returns nothing since it retriggers a hard form submission. +function fallbackPjax(options) { + var url = $.isFunction(options.url) ? options.url() : options.url, + method = options.type ? options.type.toUpperCase() : 'GET' + + var form = $('<form>', { + method: method === 'GET' ? 'GET' : 'POST', + action: url, + style: 'display:none' + }) + + if (method !== 'GET' && method !== 'POST') { + form.append($('<input>', { + type: 'hidden', + name: '_method', + value: method.toLowerCase() + })) + } + + var data = options.data + if (typeof data === 'string') { + $.each(data.split('&'), function(index, value) { + var pair = value.split('=') + form.append($('<input>', {type: 'hidden', name: pair[0], value: pair[1]})) + }) + } else if (typeof data === 'object') { + for (key in data) + form.append($('<input>', {type: 'hidden', name: key, value: data[key]})) + } + + $(document.body).append(form) + form.submit() +} + +// Internal: Generate unique id for state object. +// +// Use a timestamp instead of a counter since ids should still be +// unique across page loads. +// +// Returns Number. +function uniqueId() { + return (new Date).getTime() +} + +// Internal: Strips _pjax param from url +// +// url - String +// +// Returns String. +function stripPjaxParam(url) { + return url + .replace(/\?_pjax=[^&]+&?/, '?') + .replace(/_pjax=[^&]+&?/, '') + .replace(/[\?&]$/, '') +} + +// Internal: Parse URL components and returns a Locationish object. +// +// url - String URL +// +// Returns HTMLAnchorElement that acts like Location. +function parseURL(url) { + var a = document.createElement('a') + a.href = url + return a +} + +// Internal: Build options Object for arguments. +// +// For convenience the first parameter can be either the container or +// the options object. +// +// Examples +// +// optionsFor('#container') +// // => {container: '#container'} +// +// optionsFor('#container', {push: true}) +// // => {container: '#container', push: true} +// +// optionsFor({container: '#container', push: true}) +// // => {container: '#container', push: true} +// +// Returns options Object. +function optionsFor(container, options) { + // Both container and options + if ( container && options ) + options.container = container + + // First argument is options Object + else if ( $.isPlainObject(container) ) + options = container + + // Only container + else + options = {container: container} + + // Find and validate container + if (options.container) + options.container = findContainerFor(options.container) + + return options +} + +// Internal: Find container element for a variety of inputs. +// +// Because we can't persist elements using the history API, we must be +// able to find a String selector that will consistently find the Element. +// +// container - A selector String, jQuery object, or DOM Element. +// +// Returns a jQuery object whose context is `document` and has a selector. +function findContainerFor(container) { + container = $(container) + + if ( !container.length ) { + throw "no pjax container for " + container.selector + } else if ( container.selector !== '' && container.context === document ) { + return container + } else if ( container.attr('id') ) { + return $('#' + container.attr('id')) + } else { + throw "cant get selector for pjax container!" + } +} + +// Internal: Filter and find all elements matching the selector. +// +// Where $.fn.find only matches descendants, findAll will test all the +// top level elements in the jQuery object as well. +// +// elems - jQuery object of Elements +// selector - String selector to match +// +// Returns a jQuery object. +function findAll(elems, selector) { + return elems.filter(selector).add(elems.find(selector)); +} + +function parseHTML(html) { + return $.parseHTML(html, document, true) +} + +// Internal: Extracts container and metadata from response. +// +// 1. Extracts X-PJAX-URL header if set +// 2. Extracts inline <title> tags +// 3. Builds response Element and extracts fragment if set +// +// data - String response data +// xhr - XHR response +// options - pjax options Object +// +// Returns an Object with url, title, and contents keys. +function extractContainer(data, xhr, options) { + var obj = {} + + // Prefer X-PJAX-URL header if it was set, otherwise fallback to + // using the original requested url. + obj.url = stripPjaxParam(xhr.getResponseHeader('X-PJAX-URL') || options.requestUrl) + + // Attempt to parse response html into elements + if (/<html/i.test(data)) { + var $head = $(parseHTML(data.match(/<head[^>]*>([\s\S.]*)<\/head>/i)[0])) + var $body = $(parseHTML(data.match(/<body[^>]*>([\s\S.]*)<\/body>/i)[0])) + } else { + var $head = $body = $(parseHTML(data)) + } + + // If response data is empty, return fast + if ($body.length === 0) + return obj + + // If there's a <title> tag in the header, use it as + // the page's title. + obj.title = findAll($head, 'title').last().text() + + if (options.fragment) { + // If they specified a fragment, look for it in the response + // and pull it out. + if (options.fragment === 'body') { + var $fragment = $body + } else { + var $fragment = findAll($body, options.fragment).first() + } + + if ($fragment.length) { + obj.contents = $fragment.contents() + + // If there's no title, look for data-title and title attributes + // on the fragment + if (!obj.title) + obj.title = $fragment.attr('title') || $fragment.data('title') + } + + } else if (!/<html/i.test(data)) { + obj.contents = $body + } + + // Clean up any <title> tags + if (obj.contents) { + // Remove any parent title elements + obj.contents = obj.contents.not(function() { return $(this).is('title') }) + + // Then scrub any titles from their descendants + obj.contents.find('title').remove() + + // Gather all script[src] elements + obj.scripts = findAll(obj.contents, 'script[src]').remove() + obj.contents = obj.contents.not(obj.scripts) + } + + // Trim any whitespace off the title + if (obj.title) obj.title = $.trim(obj.title) + + return obj +} + +// Load an execute scripts using standard script request. +// +// Avoids jQuery's traditional $.getScript which does a XHR request and +// globalEval. +// +// scripts - jQuery object of script Elements +// +// Returns nothing. +function executeScriptTags(scripts) { + if (!scripts) return + + var existingScripts = $('script[src]') + + scripts.each(function() { + var src = this.src + var matchedScripts = existingScripts.filter(function() { + return this.src === src + }) + if (matchedScripts.length) return + + var script = document.createElement('script') + script.type = $(this).attr('type') + script.src = $(this).attr('src') + document.head.appendChild(script) + }) +} + +// Internal: History DOM caching class. +var cacheMapping = {} +var cacheForwardStack = [] +var cacheBackStack = [] + +// Push previous state id and container contents into the history +// cache. Should be called in conjunction with `pushState` to save the +// previous container contents. +// +// id - State ID Number +// value - DOM Element to cache +// +// Returns nothing. +function cachePush(id, value) { + cacheMapping[id] = value + cacheBackStack.push(id) + + // Remove all entires in forward history stack after pushing + // a new page. + while (cacheForwardStack.length) + delete cacheMapping[cacheForwardStack.shift()] + + // Trim back history stack to max cache length. + while (cacheBackStack.length > pjax.defaults.maxCacheLength) + delete cacheMapping[cacheBackStack.shift()] +} + +// Shifts cache from directional history cache. Should be +// called on `popstate` with the previous state id and container +// contents. +// +// direction - "forward" or "back" String +// id - State ID Number +// value - DOM Element to cache +// +// Returns nothing. +function cachePop(direction, id, value) { + var pushStack, popStack + cacheMapping[id] = value + + if (direction === 'forward') { + pushStack = cacheBackStack + popStack = cacheForwardStack + } else { + pushStack = cacheForwardStack + popStack = cacheBackStack + } + + pushStack.push(id) + if (id = popStack.pop()) + delete cacheMapping[id] +} + +// Public: Find version identifier for the initial page load. +// +// Returns String version or undefined. +function findVersion() { + return $('meta').filter(function() { + var name = $(this).attr('http-equiv') + return name && name.toUpperCase() === 'X-PJAX-VERSION' + }).attr('content') +} + +// Install pjax functions on $.pjax to enable pushState behavior. +// +// Does nothing if already enabled. +// +// Examples +// +// $.pjax.enable() +// +// Returns nothing. +function enable() { + $.fn.pjax = fnPjax + $.pjax = pjax + $.pjax.enable = $.noop + $.pjax.disable = disable + $.pjax.click = handleClick + $.pjax.submit = handleSubmit + $.pjax.reload = pjaxReload + $.pjax.defaults = { + timeout: 650, + push: true, + replace: false, + type: 'GET', + dataType: 'html', + scrollTo: 0, + maxCacheLength: 20, + version: findVersion + } + $(window).on('popstate.pjax', onPjaxPopstate) +} + +// Disable pushState behavior. +// +// This is the case when a browser doesn't support pushState. It is +// sometimes useful to disable pushState for debugging on a modern +// browser. +// +// Examples +// +// $.pjax.disable() +// +// Returns nothing. +function disable() { + $.fn.pjax = function() { return this } + $.pjax = fallbackPjax + $.pjax.enable = enable + $.pjax.disable = $.noop + $.pjax.click = $.noop + $.pjax.submit = $.noop + $.pjax.reload = function() { window.location.reload() } + + $(window).off('popstate.pjax', onPjaxPopstate) +} + + +// Add the state property to jQuery's event object so we can use it in +// $(window).bind('popstate') +if ( $.inArray('state', $.event.props) < 0 ) + $.event.props.push('state') + +// Is pjax supported by this browser? +$.support.pjax = + window.history && window.history.pushState && window.history.replaceState && + // pushState isn't reliable on iOS until 5. + !navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]|WebApps\/.+CFNetwork)/) + +$.support.pjax ? enable() : disable() + +})(jQuery); diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js index 4a919612bce3612a623f62a60085f04e883a5018..85bed3ac97ef07723a6e816b3a28b130faa69a56 100644 --- a/js/redactor-osticket.js +++ b/js/redactor-osticket.js @@ -280,4 +280,5 @@ $(function() { }; findRichtextBoxes(); $(document).ajaxStop(findRichtextBoxes); + $(document).on('pjax:success', findRichtextBoxes); }); diff --git a/scp/css/scp.css b/scp/css/scp.css index c91bc26ce61f43324985d3b231aef0cc1e072259..70695648c42e76087886d80362f3c91cd6785107 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -1505,17 +1505,20 @@ ul.progress li.no small {color:red;} } #loading, #upgrading { - border:1px solid #2a67ac; - padding: 10px 10px 10px 60px; + border:3px solid #777; + border-radius: 10px; + padding: 10px; width: 300px; - height: 100px; - background: rgb( 255, 255, 255) url('../images/FhHRx-Spinner.gif') 10px 50% no-repeat; + background: #555 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAWUlEQVQYGY2PAQoAIAgDrQf6pf7kC2vCRlRUgrjqXFrMrI3McHdKiwhpiMrTDRL4ghL8geQIsc6EOwbMcsYXhIYyUlvTgXUeS1vzkXWG8OMRXCE0b+AJAtgB7jEc+2fs6e8AAAAASUVORK5CYII=) repeat; position: fixed; display: none; z-index: 3000; + box-shadow: 0 5px 20px #001; + vertical-align: middle; } -#loading h4, #upgrading h4 { margin: 3px 0 0 0; padding: 0; color: #d80; } +#loading h1, #upgrading h4 { margin: 3px 0 0 0; padding: 0; color: #d80; } +#loading, #upgrading * { color: white; } .non-local-image:after { background: url(../images/ost-logo.png) center center no-repeat; diff --git a/scp/forms.php b/scp/forms.php index 43e66242aa85f55ea2baad290a9fa29369990055..18303449ea47b4be66e13baa7db94a79b3299a77 100644 --- a/scp/forms.php +++ b/scp/forms.php @@ -126,7 +126,8 @@ $page='dynamic-forms.inc.php'; if($form || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) $page='dynamic-form.inc.php'; -$ost->addExtraHeader('<meta name="tip-namespace" content="forms" />'); +$ost->addExtraHeader('<meta name="tip-namespace" content="forms" />', + "$('#content').data('tipNamespace', 'forms');"); $nav->setTabActive('manage'); require(STAFFINC_DIR.'header.inc.php'); require(STAFFINC_DIR.$page); diff --git a/scp/js/scp.js b/scp/js/scp.js index e2bb171c5a8a7a0e1beef6ce8a7bc1b068304d3f..60eef9e66bb15d02bf54681e57fa527da01b2a96 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -29,7 +29,7 @@ function checkbox_checker(formObj, min, max) { } -$(document).ready(function(){ +var scp_prep = function() { $("input:not(.dp):visible:enabled:first").focus(); $('table.list tbody tr:odd').addClass('odd'); @@ -241,42 +241,7 @@ $(document).ready(function(){ }) .done(function() { }) .fail(function() { }); - }); - - - - - /************ global inits *****************/ - - //Add CSRF token to the ajax requests. - // Many thanks to https://docs.djangoproject.com/en/dev/ref/contrib/csrf/ + jared. - $(document).ajaxSend(function(event, xhr, settings) { - - function sameOrigin(url) { - // url could be relative or scheme relative or absolute - var host = document.location.host; // host + port - var protocol = document.location.protocol; - var sr_origin = '//' + host; - var origin = protocol + sr_origin; - // Allow absolute or scheme relative URLs to same origin - return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || - (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || - // or any other URL that isn't scheme relative or absolute i.e - // relative. - !(/^(\/\/|http:|https:).*/.test(url)); - } - - function safeMethod(method) { - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); - } - if (!safeMethod(settings.type) && sameOrigin(settings.url)) { - xhr.setRequestHeader("X-CSRFToken", $("meta[name=csrf_token]").attr("content")); - } - - }); - - /* Get config settings from the backend */ - jQuery.fn.exists = function() { return this.length>0; }; + }); /* Multifile uploads */ var elems = $('.multifile'); @@ -292,23 +257,6 @@ $(document).ready(function(){ }); } - $.translate_format = function(str) { - var translation = { - 'd':'dd', - 'j':'d', - 'z':'o', - 'm':'mm', - 'F':'MM', - 'n':'m', - 'Y':'yy' - }; - // Change PHP formats to datepicker ones - $.each(translation, function(php, jqdp) { - str = str.replace(php, jqdp); - }); - return str; - }; - /* Datepicker */ getConfig().then(function(c) { $('.dp').datepicker({ @@ -371,6 +319,7 @@ $(document).ready(function(){ }, property: "email" }); + $('.staff-username.typeahead').typeahead({ source: function (typeahead, query) { if(query.length > 2) { @@ -418,18 +367,6 @@ $(document).ready(function(){ return false; }); - $(document).keydown(function(e) { - - if (e.keyCode == 27 && !$('#overlay').is(':hidden')) { - $('div.dialog').hide(); - $('#overlay').hide(); - - e.preventDefault(); - e.stopPropagation(); - return false; - } - }); - /* advanced search */ $('.dialog#advanced-search').css({ top : ($(window).height() / 6), @@ -449,66 +386,6 @@ $(document).ready(function(){ $('#advanced-search').show(); }); - $.dialog = function (url, codes, cb, options) { - options = options||{}; - - if (codes && !$.isArray(codes)) - codes = [codes]; - - $('.dialog#popup .body').load(url, function () { - $('#overlay').show(); - $('.dialog#popup').show({ - duration: 0, - complete: function() { if (options.onshow) options.onshow(); } - }); - $(document).off('.dialog'); - $(document).on('submit.dialog', '.dialog#popup form', function(e) { - e.preventDefault(); - var $form = $(this); - var $dialog = $form.closest('.dialog'); - $.ajax({ - type: $form.attr('method'), - url: 'ajax.php/'+$form.attr('action').substr(1), - data: $form.serialize(), - cache: false, - success: function(resp, status, xhr) { - if (xhr && xhr.status && codes - && $.inArray(xhr.status, codes) != -1) { - $('div.body', $dialog).empty(); - $dialog.hide(); - $('#overlay').hide(); - if(cb) cb(xhr); - } else { - $('div.body', $dialog).html(resp); - $('#msg_notice, #msg_error', $dialog).delay(5000).slideUp(); - } - } - }) - .done(function() { }) - .fail(function() { }); - return false; - }); - }); - if (options.onload) { options.onload(); } - }; - - $.userLookup = function (url, cb) { - $.dialog(url, 201, function (xhr) { - var user = $.parseJSON(xhr.responseText); - if (cb) cb(user); - }, { - onshow: function() { $('#user-search').focus(); } - }); - }; - - $.orgLookup = function (url, cb) { - $.dialog(url, 201, function (xhr) { - var org = $.parseJSON(xhr.responseText); - if (cb) cb(org); - }, { - onshow: function() { $('#org-search').focus(); } - }); - }; $('#advanced-search').delegate('#status', 'change', function() { switch($(this).val()) { @@ -573,6 +450,7 @@ $(document).ready(function(){ }); return ui; }; + // Sortable tables for dynamic forms objects $('.sortable-rows').sortable({ 'helper': fixHelper, @@ -584,34 +462,158 @@ $(document).ready(function(){ }); } }); +}; - //Tabs - $(document).on('click.tab', 'ul.tabs li a', function(e) { - e.preventDefault(); - if ($('.tab_content'+$(this).attr('href')).length) { - var ul = $(this).closest('ul'); - $('ul.tabs li a', ul.parent()).removeClass('active'); - $(this).addClass('active'); - $('.tab_content', ul.parent()).hide(); - $('.tab_content'+$(this).attr('href')).show(); - } +$(document).ready(scp_prep); +$(document).on('pjax:complete', scp_prep); + + /************ global inits *****************/ + +//Add CSRF token to the ajax requests. +// Many thanks to https://docs.djangoproject.com/en/dev/ref/contrib/csrf/ + jared. +$(document).ajaxSend(function(event, xhr, settings) { + + function sameOrigin(url) { + // url could be relative or scheme relative or absolute + var host = document.location.host; // host + port + var protocol = document.location.protocol; + var sr_origin = '//' + host; + var origin = protocol + sr_origin; + // Allow absolute or scheme relative URLs to same origin + return (url == origin || url.slice(0, origin.length + 1) == origin + '/') || + (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') || + // or any other URL that isn't scheme relative or absolute i.e + // relative. + !(/^(\/\/|http:|https:).*/.test(url)); + } + + function safeMethod(method) { + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + if (!safeMethod(settings.type) && sameOrigin(settings.url)) { + xhr.setRequestHeader("X-CSRFToken", $("meta[name=csrf_token]").attr("content")); + } + +}); + +/* Get config settings from the backend */ +jQuery.fn.exists = function() { return this.length>0; }; + +$.translate_format = function(str) { + var translation = { + 'd':'dd', + 'j':'d', + 'z':'o', + 'm':'mm', + 'F':'MM', + 'n':'m', + 'Y':'yy' + }; + // Change PHP formats to datepicker ones + $.each(translation, function(php, jqdp) { + str = str.replace(php, jqdp); }); + return str; +}; +$(document).keydown(function(e) { + + if (e.keyCode == 27 && !$('#overlay').is(':hidden')) { + $('div.dialog').hide(); + $('#overlay').hide(); - //Collaborators - $(document).on('click', 'a.collaborator, a.collaborators', function(e) { e.preventDefault(); - var url = 'ajax.php/'+$(this).attr('href').substr(1); - $.dialog(url, 201, function (xhr) { - $('input#emailcollab').show(); - $('#recipients').text(xhr.responseText); - $('.tip_box').remove(); - }, { - onshow: function() { $('#user-search').focus(); } - }); + e.stopPropagation(); return false; + } +}); + +$.dialog = function (url, codes, cb, options) { + options = options||{}; + + if (codes && !$.isArray(codes)) + codes = [codes]; + + $('.dialog#popup .body').load(url, function () { + $('#overlay').show(); + $('.dialog#popup').show({ + duration: 0, + complete: function() { if (options.onshow) options.onshow(); } + }); + $(document).off('.dialog'); + $(document).on('submit.dialog', '.dialog#popup form', function(e) { + e.preventDefault(); + var $form = $(this); + var $dialog = $form.closest('.dialog'); + $.ajax({ + type: $form.attr('method'), + url: 'ajax.php/'+$form.attr('action').substr(1), + data: $form.serialize(), + cache: false, + success: function(resp, status, xhr) { + if (xhr && xhr.status && codes + && $.inArray(xhr.status, codes) != -1) { + $('div.body', $dialog).empty(); + $dialog.hide(); + $('#overlay').hide(); + if(cb) cb(xhr); + } else { + $('div.body', $dialog).html(resp); + $('#msg_notice, #msg_error', $dialog).delay(5000).slideUp(); + } + } + }) + .done(function() { }) + .fail(function() { }); + return false; + }); }); + if (options.onload) { options.onload(); } + }; + +$.userLookup = function (url, cb) { + $.dialog(url, 201, function (xhr) { + var user = $.parseJSON(xhr.responseText); + if (cb) cb(user); + }, { + onshow: function() { $('#user-search').focus(); } + }); +}; + +$.orgLookup = function (url, cb) { + $.dialog(url, 201, function (xhr) { + var org = $.parseJSON(xhr.responseText); + if (cb) cb(org); + }, { + onshow: function() { $('#org-search').focus(); } + }); +}; + +//Tabs +$(document).on('click.tab', 'ul.tabs li a', function(e) { + e.preventDefault(); + if ($('.tab_content'+$(this).attr('href')).length) { + var ul = $(this).closest('ul'); + $('ul.tabs li a', ul.parent()).removeClass('active'); + $(this).addClass('active'); + $('.tab_content', ul.parent()).hide(); + $('.tab_content'+$(this).attr('href')).show(); + } }); +//Collaborators +$(document).on('click', 'a.collaborator, a.collaborators', function(e) { + e.preventDefault(); + var url = 'ajax.php/'+$(this).attr('href').substr(1); + $.dialog(url, 201, function (xhr) { + $('input#emailcollab').show(); + $('#recipients').text(xhr.responseText); + $('.tip_box').remove(); + }, { + onshow: function() { $('#user-search').focus(); } + }); + return false; + }); + // NOTE: getConfig should be global getConfig = (function() { var dfd = $.Deferred(), @@ -623,8 +625,35 @@ getConfig = (function() { dataType: 'json', success: function (json_config) { dfd.resolve(json_config); + }, + error: function() { + requested = null; } }); return dfd; } })(); + +$(document).on('pjax:start', function() { + // Don't show the spinner on back button + if (event instanceof PopStateEvent) + return; + + clearInterval(window.ticket_refresh); + $('#loading').show().css({opacity:0.7}); + // Clear all timeouts + var id = window.setTimeout(function() {}, 0); + while (id--) { + window.clearTimeout(id); + } +}); +$(document).on('pjax:complete', function() { + $('#loading').hide().css({opacity:1}); +}); +$(document).on('click', 'a', function() { + var ul = $(this).closest('ul'); + if (ul.is('#sub_nav')) { + $('a.active', ul).removeClass('active'); + $(this).addClass('active'); + } +}); diff --git a/scp/js/ticket.js b/scp/js/ticket.js index f16576d6f84320f8f3dc5c928a8d36f7cf950032..b590e5c017adfffcdcd8360b55ce58c8c1aacbb1 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -265,7 +265,7 @@ $.autoLock = autoLock; UI & form events */ -jQuery(function($) { +var ticket_onload = function($) { $('#response_options form').hide(); $('#ticket_notes').hide(); if(location.hash != "" && $('#response_options '+location.hash).length) { @@ -407,7 +407,9 @@ jQuery(function($) { // TODO: Add a hover-button to show just one image }); }); -}); +}; +$(ticket_onload); +$(document).on('pjax:success', function() { ticket_onload(jQuery); }); showImagesInline = function(urls, thread_id) { var selector = (thread_id == undefined) @@ -435,7 +437,7 @@ showImagesInline = function(urls, thread_id) { } ).append($('<div class="caption">') .append('<span class="filename">'+info.filename+'</span>') - .append('<a href="'+info.download_url+'" class="action-button"><i class="icon-download-alt"></i> Download</a>') + .append('<a href="'+info.download_url+'" class="action-button no-pjax"><i class="icon-download-alt"></i> Download</a>') ); e.data('wrapped', true); } diff --git a/scp/js/tips.js b/scp/js/tips.js index 9fdd2261701abc3c60012dd54a6ebe10a6b548fc..a4a9e8b4c61cb7499c9cefc9b123ad29a0ec8390 100644 --- a/scp/js/tips.js +++ b/scp/js/tips.js @@ -24,20 +24,26 @@ jQuery(function() { }); }, getHelpTips = (function() { - var dfd = $.Deferred(), - requested = false, - namespace = $('meta[name=tip-namespace]').attr('content'); - return function() { - if (namespace && dfd.state() != 'resolved' && !requested) - requested = $.ajax({ + var dfd, cache = {}; + return function(namespace) { + var namespace = namespace + || $('#content').data('tipNamespace') + || $('meta[name=tip-namespace]').attr('content'); + if (!namespace) + return false; + else if (!cache[namespace]) + cache[namespace] = { + dfd: dfd = $.Deferred(), + ajax: $.ajax({ url: "ajax.php/help/tips/" + namespace, dataType: 'json', - success: function (json_config) { - dfd.resolve(json_config); - } - }); - return dfd; - } + success: $.proxy(function (json_config) { + this.resolve(json_config); + }, dfd) + }) + } + return cache[namespace].dfd; + }; })(); //Generic tip. diff --git a/scp/settings.php b/scp/settings.php index a4243fd957fb780eea3b151fd96c8b9770ef9ccb..96ee4b97c20c50fde39cce47bc9f28edeb83a1c9 100644 --- a/scp/settings.php +++ b/scp/settings.php @@ -48,7 +48,8 @@ if($page && $_POST && !$errors) { } $config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfigInfo()); -$ost->addExtraHeader('<meta name="tip-namespace" content="'.$page[1].'" />'); +$ost->addExtraHeader('<meta name="tip-namespace" content="'.$page[1].'" />', + "$('#content').data('tipNamespace', '".$page[1]."');"); $nav->setTabActive('settings', ('settings.php?t='.$target)); require_once(STAFFINC_DIR.'header.inc.php'); diff --git a/scp/tickets.php b/scp/tickets.php index 1655e7027a98ca7af715d3213b4b68c318134692..07ee1ed840cc18a2c28d54ab24d945533dcc24dd 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -616,10 +616,25 @@ if($ticket) { //set refresh rate if the user has it configured if(!$_POST && !$_REQUEST['a'] && ($min=$thisstaff->getRefreshRate())) - $ost->addExtraHeader('<meta http-equiv="refresh" content="'.($min*60).'" />'); + $ost->addExtraHeader('', + "window.ticket_refresh = setTimeout(function() { $.pjax({url: document.location.href, container:'#content'});}," + .($min*60000).");"); } +$ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>'); + require_once(STAFFINC_DIR.'header.inc.php'); require_once(STAFFINC_DIR.$inc); require_once(STAFFINC_DIR.'footer.inc.php'); + +if (isset($_SERVER['HTTP_X_PJAX'])) { + // Update the ticket queue counts in the navigation + ob_start(); + include STAFFINC_DIR . "templates/sub-navigation.tmpl.php"; + $nav_content = ob_get_clean(); +?> +<script type="text/javascript"> + $('#sub_nav').html(<?php echo JsonDataEncoder::encode($nav_content); ?>); +</script><? +} ?>