diff --git a/include/class.thread.php b/include/class.thread.php index 753b6d803744fdb7ef9d103d672c1bf73b073846..6018045eac2164f81b448ebf9e848c302636c7db 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -212,7 +212,12 @@ class Thread extends VerySimpleModel { return true; } // Render thread - function render($type=false, $mode=self::MODE_STAFF) { + function render($type=false, $options=array()) { + + $mode = $options['mode'] ?: self::MODE_STAFF; + + // Register thread actions prior to rendering the thread. + include_once INCLUDE_DIR . 'class.thread_actions.php'; $entries = $this->getEntries(); if ($type && is_array($type)) diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php index 9b1fc704db6ed350c06b932abd3fcb93bcf00bf6..b91b91d74ccb7a32d7949e3ce64f3a1eb2ea7db1 100644 --- a/include/client/templates/thread-entries.tmpl.php +++ b/include/client/templates/thread-entries.tmpl.php @@ -6,6 +6,10 @@ $events = $events->getIterator(); $events->rewind(); $event = $events->current(); +$htmlId = $options['html-id'] ?: ('thread-'.$this->getId()); +?> +<div id="<?php echo $htmlId; ?>" data-thread-id="<?php echo $this->getId(); ?>"> +<?php if (count($entries)) { // Go through all the entries and bucket them by time frame $buckets = array(); @@ -47,3 +51,5 @@ while ($event) { if (count($entries) + count($events) == 0) { echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>'; } +?> +</div> diff --git a/include/client/view.inc.php b/include/client/view.inc.php index 2be53bc477943706361921063d776169e1398dce..97c3bb8ef5eb52e5d279b44aeee57da46a7604b5 100644 --- a/include/client/view.inc.php +++ b/include/client/view.inc.php @@ -127,11 +127,12 @@ foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) { </table> <br> -<div id="ticketThread"> <?php - $ticket->getThread()->render(array('M', 'R'), Thread::MODE_CLIENT); + $ticket->getThread()->render(array('M', 'R'), array( + 'mode' => Thread::MODE_CLIENT, + 'html-id' => 'ticketThread') + ); ?> -</div> <div class="clear" style="padding-bottom:10px;"></div> <?php if($errors['err']) { ?> diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index 7ac199444b8b2607346451a97e5435cb5dda0753..b1e3a83bd155a3c0f58867a112a7c9ad33518b4e 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -3,47 +3,75 @@ $events = $events->order_by('id'); $events = $events->getIterator(); $events->rewind(); $event = $events->current(); +$htmlId = $options['html-id'] ?: ('thread-'.$this->getId()); +?> +<div id="<?php echo $htmlId; ?>" data-thread-id="<?php echo $this->getId(); ?>"> + <div id="thread-items"> + <?php + if (count($entries)) { + // Go through all the entries and bucket them by time frame + $buckets = array(); + $rel = 0; + foreach ($entries as $i=>$E) { + // First item _always_ shows up + if ($i != 0) + // Set relative time resolution to 12 hours + $rel = Format::relativeTime(Misc::db2gmtime($E->created, false, 43200)); + $buckets[$rel][] = $E; + } -if (count($entries)) { - // Go through all the entries and bucket them by time frame - $buckets = array(); - $rel = 0; - foreach ($entries as $i=>$E) { - // First item _always_ shows up - if ($i != 0) - // Set relative time resolution to 12 hours - $rel = Format::relativeTime(Misc::db2gmtime($E->created, false, 43200)); - $buckets[$rel][] = $E; - } - - // Go back through the entries and render them on the page - $i = 0; - foreach ($buckets as $rel=>$entries) { - // TODO: Consider adding a date boundary to indicate significant - // changes in dates between thread items. - foreach ($entries as $entry) { - // Emit all events prior to this entry - while ($event && $event->timestamp < $entry->created) { - $event->render(ThreadEvent::MODE_STAFF); - $events->next(); - $event = $events->current(); + // Go back through the entries and render them on the page + foreach ($buckets as $rel=>$entries) { + // TODO: Consider adding a date boundary to indicate significant + // changes in dates between thread items. + foreach ($entries as $entry) { + // Emit all events prior to this entry + while ($event && $event->timestamp < $entry->created) { + $event->render(ThreadEvent::MODE_STAFF); + $events->next(); + $event = $events->current(); + } + ?><div id="thread-entry-<?php echo $entry->getId(); ?>"><?php + include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; + ?></div><?php } - ?><div id="thread-entry-<?php echo $entry->getId(); ?>"><?php - include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; - ?></div><?php } - $i++; } -} -// Emit all other events -while ($event) { - $event->render(ThreadEvent::MODE_STAFF); - $events->next(); - $event = $events->current(); -} + // Emit all other events + while ($event) { + $event->render(ThreadEvent::MODE_STAFF); + $events->next(); + $event = $events->current(); + } + // This should never happen + if (count($entries) + count($events) == 0) { + echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>'; + } + ?> + </div> +</div> +<script type="text/javascript"> + $(function() { + var container = '<?php echo $htmlId; ?>'; + + // Set inline image urls. + <?php + $urls = array(); + foreach (AttachmentFile::objects()->filter(array( + 'attachments__thread_entry__thread__id' => $this->getId(), + 'attachments__inline' => true, + )) as $file) { + $urls[strtolower($file->getKey())] = array( + 'download_url' => $file->getDownloadUrl(), + 'filename' => $file->name, + ); -// This should never happen -if (count($entries) + count($events) == 0) { - echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>'; -} + } + ?> + $('#'+container).data('imageUrls', <?php echo JsonDataEncoder::encode($urls); ?>); + // Trigger thread processing. + if ($.thread) + $.thread.onLoad(container); + }); +</script> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 84bbc59833b18068c6e5995e0e54265369e1f2a1..ecad25ee7d8a79e59575551cef4790ecf459da8a 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -1,6 +1,4 @@ <?php -include_once INCLUDE_DIR . 'class.thread_actions.php'; - //Note that ticket obj is initiated in tickets.php. if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path'); @@ -434,10 +432,16 @@ $tcount = $ticket->getThreadEntries($types)->count(); </ul> <div id="ticket_tabs_container"> - <div id="ticket_thread" data-thread-id="<?php echo $ticket->getThread()->getId(); ?>" class="tab_content"> - <div id="thread-items"> - <?php $ticket->getThread()->render(array('M', 'R', 'N')); ?> - </div> +<div id="ticket_thread" class="tab_content"> +<?php + // Render ticket thread + $ticket->getThread()->render( + array('M', 'R', 'N'), + array( + 'html-id' => 'ticketThread', + 'mode' => Thread::MODE_STAFF) + ); +?> <div class="clear"></div> <?php if($errors['err']) { ?> <div id="msg_error"><?php echo $errors['err']; ?></div> @@ -1035,18 +1039,4 @@ $(function() { }(); <?php } ?> }); - -<?php -// Hover support for all inline images -$urls = array(); -foreach (AttachmentFile::objects()->filter(array( - 'attachments__thread_entry__thread__id' => $ticket->getThreadId(), - 'attachments__inline' => true, -)) as $file) { - $urls[strtolower($file->getKey())] = array( - 'download_url' => $file->getDownloadUrl(), - 'filename' => $file->name, - ); -} ?> -$('#ticket_thread').data('imageUrls', <?php echo JsonDataEncoder::encode($urls); ?>); </script> diff --git a/scp/js/thread.js b/scp/js/thread.js new file mode 100644 index 0000000000000000000000000000000000000000..62568ce27ada399fbf3fadfed5128ae267176495 --- /dev/null +++ b/scp/js/thread.js @@ -0,0 +1,180 @@ +/********************************************************************* + thread.js + + Thread JS untils + Copyright (c) 2014 osTicket + http://www.osticket.com + + Released under the GNU General Public License WITHOUT ANY WARRANTY. + + vim: expandtab sw=4 ts=4 sts=4: +**********************************************************************/ + +var thread = { + + options: { + autoScroll: true, + showimages: false + }, + + scrollTo: function (entry) { + + if (!entry) return; + + var frame = 0; + $('html, body').delay(500).animate({ + scrollTop: entry.offset().top - 50, + }, { + duration: 750, + step: function(now, fx) { + // Recalc end target every few frames + if (++frame % 6 == 0) + fx.end = entry.offset().top - 50; + } + }); + }, + + showExternalImage: function(div) { + var $div = $(div), + $img = $div.append($('<img>') + .attr('src', $div.data('src')) + .attr('alt', $div.attr('alt')) + .attr('title', $div.attr('title')) + .attr('style', $div.data('style')) + ); + if ($div.attr('width')) + $img.width($div.attr('width')); + if ($div.attr('height')) + $img.height($div.attr('height')); + }, + + externalImages: function() { + + // Optionally show external images + $('.thread-entry', this.options.container).each(function(i, te) { + var extra = $(te).find('.textra'), + imgs = $(te).find('.non-local-image[data-src]'); + + if (!extra || !imgs.length) + return; + + // Add Show Images buttons + extra.append($('<a>') + .addClass("action-button pull-right show-images") + .css({'font-weight':'normal'}) + .text(' ' + __('Show Images')) + .click(function(ev) { + imgs.each(function(i, img) { + this.showExternalImage(img); + $(img).removeClass('non-local-image') + // Remove placeholder sizing + .css({'display':'inline-block'}) + .width('auto') + .height('auto') + .removeAttr('width') + .removeAttr('height'); + extra.find('.show-images').hide(); + }); + }) + .prepend($('<i>') + .addClass('icon-picture') + ) + ); + + // Show placeholders + imgs.each(function(i, img) { + var $img = $(img); + // Save a copy of the original styling + $img.data('style', $img.attr('style')); + $img.removeAttr('style'); + // If the image has a 'height' attribute, use it, otherwise, use + // 40px + $img.height(($img.attr('height') || '40') + 'px'); + // Ensure the image placeholder is visible width-wise + if (!$img.width()) + $img.width(($img.attr('width') || '80') + 'px'); + // TODO: Add a hover-button to show just one image + }); + }); + }, + + inlineImages: function (entry_id) { + // TODO: use entry selector or object instead of ID + var selector = (entry_id == undefined) + ? '.thread-body img[data-cid]' + : '.thread-body#thread-id-'+entry_id+' img[data-cid]'; + + // Get urls + if (!(urls=this.options.container.data('imageUrls'))) + return; + + $(selector, this.options.container).each(function(i, el) { + var e = $(el), + cid = e.data('cid').toLowerCase(), + info = urls[cid]; + if (info && !e.data('wrapped')) { + // Add a hover effect with the filename + var timeout, caption = $('<div class="image-hover">') + .css({'float':e.css('float')}); + e.wrap(caption).parent() + .hover( + function() { + var self = this; + timeout = setTimeout( + function() { $(self).find('.caption').slideDown(250); }, + 500); + }, + function() { + clearTimeout(timeout); + $(this).find('.caption').slideUp(250); + } + ).append($('<div class="caption">') + .append('<span class="filename">'+info.filename+'</span>') + .append($('<a href="'+info.download_url+'" class="action-button pull-right no-pjax"><i class="icon-download-alt"></i> '+__('Download')+'</a>') + .attr('download', info.filename) + ) + ); + e.data('wrapped', true); + } + }); + }, + + prepImages: function() { + + // TODO: Check config options + this.externalImages(); + this.inlineImages(); + }, + + onLoad: function (container, options) { + + // See if thread container is valid + $container = $('#'+container); + if (!$container || !$container.length) + return; + + // set options + this.options.container = $container; + $.extend(this.options, options); + + // Prep images + this.prepImages(); + + // Auto scroll to the last entry if autoScroll is enabled. + if (this.options.autoScroll === true) { + // Find the last entry to scroll to. + var e = $('.thread-entry', $container).filter(':visible').last(); + if (e.length) + this.scrollTo(e); + } + + // Open thread body links in a new tab/window + $('div.thread-body a', $container).each(function() { + $(this).attr('target', '_blank'); + }); + } +}; + +// Set thread as JQuery object +$.thread = thread; + diff --git a/scp/js/ticket.js b/scp/js/ticket.js index de289ca296892b8fb7196599a398d8105480c429..c48209a3419bb2e51ed749d5205f52dc0694c36e 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -293,20 +293,6 @@ $.autoLock = autoLock; /* UI & form events */ -$.showNonLocalImage = function(div) { - var $div = $(div), - $img = $div.append($('<img>') - .attr('src', $div.data('src')) - .attr('alt', $div.attr('alt')) - .attr('title', $div.attr('title')) - .attr('style', $div.data('style')) - ); - if ($div.attr('width')) - $img.width($div.attr('width')); - if ($div.attr('height')) - $img.height($div.attr('height')); -}; - $.showImagesInline = function(urls, thread_id) { var selector = (thread_id == undefined) ? '.thread-body img[data-cid]' @@ -358,7 +344,7 @@ $.refreshTicketView = function(interval) { } var ticket_onload = function($) { - if (0 === $('#ticket_thread').length) + if (0 === $('#ticketThread').length) return; //Start watching the form for activity. @@ -397,66 +383,6 @@ var ticket_onload = function($) { $cc.show(); }); - // Optionally show external images - $('.thread-entry').each(function(i, te) { - var extra = $(te).find('.textra'), - imgs = $(te).find('.non-local-image[data-src]'); - if (!extra) return; - if (!imgs.length) return; - extra.append($('<a>') - .addClass("action-button pull-right show-images") - .css({'font-weight':'normal'}) - .text(' ' + __('Show Images')) - .click(function(ev) { - imgs.each(function(i, img) { - $.showNonLocalImage(img); - $(img).removeClass('non-local-image') - // Remove placeholder sizing - .css({'display':'inline-block'}) - .width('auto') - .height('auto') - .removeAttr('width') - .removeAttr('height'); - extra.find('.show-images').hide(); - }); - }) - .prepend($('<i>') - .addClass('icon-picture') - ) - ); - imgs.each(function(i, img) { - var $img = $(img); - // Save a copy of the original styling - $img.data('style', $img.attr('style')); - $img.removeAttr('style'); - // If the image has a 'height' attribute, use it, otherwise, use - // 40px - $img.height(($img.attr('height') || '40') + 'px'); - // Ensure the image placeholder is visible width-wise - if (!$img.width()) - $img.width(($img.attr('width') || '80') + 'px'); - // TODO: Add a hover-button to show just one image - }); - }); - - $.showImagesInline($('#ticket_thread').data('imageUrls')); - - var last_entry = $('#ticket_thread .thread-entry').last(), - frame = 0; - $('html, body').delay(500).animate({ - scrollTop: last_entry.offset().top - 50, - }, { - duration: 750, - step: function(now, fx) { - // Recalc end target every few frames - if (++frame % 6 == 0) - fx.end = last_entry.offset().top - 50; - } - }); - - $('div.thread-body a').each(function() { - $(this).attr('target', '_blank'); - }); }; $(ticket_onload); $(document).on('pjax:success', function() { ticket_onload(jQuery); }); diff --git a/scp/tasks.php b/scp/tasks.php index 2f4791356b83b0bc46dcde1b96b4e36624828f8f..c22fa5bdb39671a5e47e835d8a8447711b022f6a 100644 --- a/scp/tasks.php +++ b/scp/tasks.php @@ -154,6 +154,7 @@ if ($thisstaff->hasPerm(TaskModel::PERM_CREATE)) { $ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>'); +$ost->addExtraHeader('<script type="text/javascript" src="js/thread.js"></script>'); $ost->addExtraHeader('<meta name="tip-namespace" content="tasks.queue" />', "$('#content').data('tipNamespace', 'tasks.queue');"); diff --git a/scp/tickets.php b/scp/tickets.php index 932e70c6481959080e24de47d63f67221a1fb05d..8e1d45185b6eace1655b12fe900be0a825cd95ad 100644 --- a/scp/tickets.php +++ b/scp/tickets.php @@ -494,6 +494,7 @@ if ($thisstaff->hasPerm(TicketModel::PERM_CREATE)) { $ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>'); +$ost->addExtraHeader('<script type="text/javascript" src="js/thread.js"></script>'); $ost->addExtraHeader('<meta name="tip-namespace" content="tickets.queue" />', "$('#content').data('tipNamespace', 'tickets.queue');");