diff --git a/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
index 1f09bf11e..897ed452c 100644
--- a/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
+++ b/app/assets/javascripts/app/controllers/agent_ticket_zoom.js.coffee
@@ -31,7 +31,9 @@ class Index extends App.Controller
cache = App.Store.get( @key )
if cache
@load(cache)
- @fetch(@ticket_id)
+ update = =>
+ @fetch(@ticket_id)
+ @interval( update, 30000, 'zoom_check' )
fetch: (ticket_id) ->
@@ -44,6 +46,17 @@ class Index extends App.Controller
view: @view
processData: true
success: (data, status, xhr) =>
+ if _.isEqual( @dataLastCall, data)
+ return
+ if @dataLastCall && $('[name="body"]').val()
+ App.Event.trigger 'notify', {
+ type: 'success'
+ msg: App.i18n.translateContent('Ticket has changed!')
+ timeout: 30000
+ }
+ return
+ @dataLastCall = data
+
@load(data)
App.Store.write( @key, data )
)
diff --git a/app/assets/javascripts/app/controllers/text_module_widget.js.coffee b/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
index 66ade4660..4105a590e 100644
--- a/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
+++ b/app/assets/javascripts/app/controllers/text_module_widget.js.coffee
@@ -1,6 +1,35 @@
-$ = jQuery.sub()
-
class App.TextModuleUI extends App.Controller
+ constructor: ->
+ super
+ ui = @
+ values = []
+ all = App.Collection.all( type: 'TextModule' )
+ for item in all
+ if item.active is true
+ contentNew = item.content.replace( /<%=\s{0,2}(.+?)\s{0,2}%>/g, ( all, key ) ->
+ key = key.replace( /@/g, 'ui.data.' )
+ varString = "#{key}" + ''
+ try
+ key = eval (varString)
+ catch error
+ # console.log( "tag replacement: " + error )
+ key = ''
+ return key
+ )
+ value = { val: contentNew, keywords: item.keywords || item.name }
+ values.push value
+
+ customItemTemplate = "
"
+ elementFactory = (element, e) ->
+ template = $(customItemTemplate).find('span')
+ .text(e.val).end()
+ .find('small')
+ .text("(" + e.keywords + ")").end()
+ element.append(template)
+ $('textarea').sew({values: values, token: ':', elementFactory: elementFactory })
+
+
+class App.TextModuleUIOld extends App.Controller
events:
'click [data-type=save]': 'create',
'click [data-type=text_module_delete]': 'delete',
diff --git a/app/assets/javascripts/app/lib/base/jquery.caretposition.js b/app/assets/javascripts/app/lib/base/jquery.caretposition.js
new file mode 100644
index 000000000..7467500db
--- /dev/null
+++ b/app/assets/javascripts/app/lib/base/jquery.caretposition.js
@@ -0,0 +1,185 @@
+/**
+ * jQuery plugin for getting position of cursor in textarea
+
+ * @license under GNU license
+ * @author Bevis Zhao (i@bevis.me, http://bevis.me)
+ */
+$(function() {
+
+ var calculator = {
+ // key styles
+ primaryStyles: ['fontFamily', 'fontSize', 'fontWeight', 'fontVariant', 'fontStyle',
+ 'paddingLeft', 'paddingTop', 'paddingBottom', 'paddingRight',
+ 'marginLeft', 'marginTop', 'marginBottom', 'marginRight',
+ 'borderLeftColor', 'borderTopColor', 'borderBottomColor', 'borderRightColor',
+ 'borderLeftStyle', 'borderTopStyle', 'borderBottomStyle', 'borderRightStyle',
+ 'borderLeftWidth', 'borderTopWidth', 'borderBottomWidth', 'borderRightWidth',
+ 'line-height', 'outline'],
+
+ specificStyle: {
+ 'word-wrap': 'break-word',
+ 'overflow-x': 'hidden',
+ 'overflow-y': 'auto'
+ },
+
+ simulator : $('').css({
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ visibility: 'hidden'
+ }).appendTo(document.body),
+
+ toHtml : function(text) {
+ return text.replace(//g,'>').replace(/\n/g, '
')
+ .split(' ').join(' ');
+ },
+ // calculate position
+ getCaretPosition: function() {
+ var cal = calculator, self = this, element = self[0], elementOffset = self.offset();
+
+ // IE has easy way to get caret offset position
+ if ($.browser.msie && $.browser.version <= 9) {
+ // must get focus first
+ element.focus();
+ var range = document.selection.createRange();
+ $('#hskeywords').val(element.scrollTop);
+ return {
+ left: range.boundingLeft - elementOffset.left,
+ top: parseInt(range.boundingTop) - elementOffset.top + element.scrollTop
+ + document.documentElement.scrollTop + parseInt(self.getComputedStyle("fontSize"))
+ };
+ }
+
+ cal.simulator.empty();
+ // clone primary styles to imitate textarea
+ $.each(cal.primaryStyles, function(index, styleName) {
+ self.cloneStyle(cal.simulator, styleName);
+ });
+
+ // caculate width and height
+ cal.simulator.css($.extend({
+ 'width': self.width(),
+ 'height': self.height()
+ }, cal.specificStyle));
+
+ var value = (self.val() || self.text()), cursorPosition = self.getCursorPosition();
+ var beforeText = value.substring(0, cursorPosition),
+ afterText = value.substring(cursorPosition);
+
+ var before = $('').html(cal.toHtml(beforeText)),
+ focus = $(''),
+ after = $('').html(cal.toHtml(afterText));
+
+ cal.simulator.append(before).append(focus).append(after);
+ var focusOffset = focus.offset(), simulatorOffset = cal.simulator.offset();
+ // alert(focusOffset.left + ',' + simulatorOffset.left + ',' + element.scrollLeft);
+ return {
+ top: focusOffset.top - simulatorOffset.top - element.scrollTop
+ // calculate and add the font height except Firefox
+ + ($.browser.mozilla ? 0 : parseInt(self.getComputedStyle("fontSize"))),
+ left: focus[0].offsetLeft - cal.simulator[0].offsetLeft - element.scrollLeft
+ };
+ }
+ };
+
+ $.fn.extend({
+ setCursorPosition : function(position){
+ if(this.length == 0) return this;
+ return $(this).setSelection(position, position);
+ },
+ setSelection: function(selectionStart, selectionEnd) {
+ if(this.length == 0) return this;
+ input = this[0];
+
+ if (input.createTextRange) {
+ var range = input.createTextRange();
+ range.collapse(true);
+ range.moveEnd('character', selectionEnd);
+ range.moveStart('character', selectionStart);
+ range.select();
+ } else if (input.setSelectionRange) {
+ input.focus();
+ input.setSelectionRange(selectionStart, selectionEnd);
+ } else {
+ var el = this.get(0);
+
+ var range = document.createRange();
+ range.collapse(true);
+ range.setStart(el.childNodes[0], selectionStart);
+ range.setEnd(el.childNodes[0], selectionEnd);
+
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+
+ return this;
+ },
+ getComputedStyle: function(styleName) {
+ if (this.length == 0) return;
+ var thiz = this[0];
+ var result = this.css(styleName);
+ result = result || ($.browser.msie ?
+ thiz.currentStyle[styleName]:
+ document.defaultView.getComputedStyle(thiz, null)[styleName]);
+ return result;
+ },
+ // easy clone method
+ cloneStyle: function(target, styleName) {
+ var styleVal = this.getComputedStyle(styleName);
+ if (!!styleVal) {
+ $(target).css(styleName, styleVal);
+ }
+ },
+ cloneAllStyle: function(target, style) {
+ var thiz = this[0];
+ for (var styleName in thiz.style) {
+ var val = thiz.style[styleName];
+ typeof val == 'string' || typeof val == 'number'
+ ? this.cloneStyle(target, styleName)
+ : NaN;
+ }
+ },
+ getCursorPosition : function() {
+ var element = input = this[0];
+ var value = (input.value || input.innerText)
+
+ if(!this.data("lastCursorPosition")){
+ this.data("lastCursorPosition",0);
+ }
+
+ var lastCursorPosition = this.data("lastCursorPosition");
+
+ if (document.selection) {
+ input.focus();
+ var sel = document.selection.createRange();
+ var selLen = document.selection.createRange().text.length;
+ sel.moveStart('character', -value.length);
+ lastCursorPosition = sel.text.length - selLen;
+ } else if (input.selectionStart || input.selectionStart == '0') {
+ return input.selectionStart;
+ } else if (typeof window.getSelection != "undefined" && window.getSelection().rangeCount>0) {
+ try{
+ var selection = window.getSelection();
+ var range = selection.getRangeAt(0);
+ var preCaretRange = range.cloneRange();
+ preCaretRange.selectNodeContents(element);
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
+ lastCursorPosition = preCaretRange.toString().length;
+ }catch(e){
+ lastCursorPosition = this.data("lastCursorPosition");
+ }
+ } else if (typeof document.selection != "undefined" && document.selection.type != "Control") {
+ var textRange = document.selection.createRange();
+ var preCaretTextRange = document.body.createTextRange();
+ preCaretTextRange.moveToElementText(element);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ lastCursorPosition = preCaretTextRange.text.length;
+ }
+
+ this.data("lastCursorPosition",lastCursorPosition);
+ return lastCursorPosition;
+ },
+ getCaretPosition: calculator.getCaretPosition
+ });
+});
diff --git a/app/assets/javascripts/app/lib/base/jquery.sew.js b/app/assets/javascripts/app/lib/base/jquery.sew.js
new file mode 100644
index 000000000..a9bd5f608
--- /dev/null
+++ b/app/assets/javascripts/app/lib/base/jquery.sew.js
@@ -0,0 +1,275 @@
+/**
+ * jQuery plugin for getting position of cursor in textarea
+ * @license under dfyw (do the fuck you want)
+ * @author leChantaux (@leChantaux)
+ */
+
+(function ($, window, undefined) {
+ // Create the defaults once
+ var elementFactory = function (element, value) {
+ element.text(value.val);
+ };
+
+ var pluginName = 'sew',
+ document = window.document,
+ defaults = {
+ token: '@',
+ elementFactory: elementFactory,
+ values: [],
+ unique: false,
+ repeat: true
+ };
+
+ function Plugin(element, options) {
+ this.element = element;
+ this.$element = $(element);
+ this.$itemList = $(Plugin.MENU_TEMPLATE);
+
+ this.options = $.extend({}, defaults, options);
+ this.reset();
+
+ this._defaults = defaults;
+ this._name = pluginName;
+
+// this.expression = new RegExp('(?:^|\\b|\\s)' + this.options.token + '([\\w.]*)$');
+ this.expression = new RegExp('' + this.options.token + '([\\w.]*)$');
+ this.cleanupHandle = null;
+
+ this.init();
+ }
+
+ Plugin.MENU_TEMPLATE = "";
+
+ Plugin.ITEM_TEMPLATE = '';
+
+ Plugin.KEYS = [40, 38, 13, 27, 9];
+
+ Plugin.prototype.init = function () {
+ if(this.options.values.length < 1) return;
+
+ this.$element
+ .bind('keyup', $.proxy(this.onKeyUp, this))
+ .bind('keydown', $.proxy(this.onKeyDown, this))
+ .bind('focus', $.proxy(this.renderElements, this, this.options.values))
+ .bind('blur', $.proxy(this.remove, this));
+ };
+
+ Plugin.prototype.reset = function () {
+ if(this.options.unique) {
+ this.options.values = Plugin.getUniqueElements(this.options.values);
+ }
+
+ this.index = 0;
+ this.matched = false;
+ this.dontFilter = false;
+ this.lastFilter = undefined;
+ this.filtered = this.options.values.slice(0);
+ };
+
+ Plugin.prototype.next = function () {
+ this.index = (this.index + 1) % this.filtered.length;
+ this.hightlightItem();
+ };
+
+ Plugin.prototype.prev = function () {
+ this.index = (this.index + this.filtered.length - 1) % this.filtered.length;
+ this.hightlightItem();
+ };
+
+ Plugin.prototype.select = function () {
+ this.replace(this.filtered[this.index].val);
+ this.hideList();
+ };
+
+ Plugin.prototype.remove = function () {
+ this.$itemList.fadeOut('slow');
+
+ this.cleanupHandle = window.setTimeout($.proxy(function () {
+ this.$itemList.remove();
+ }, this), 1000);
+ };
+
+ Plugin.prototype.replace = function (replacement) {
+ var startpos = this.$element.getCursorPosition();
+// var separator = startpos === 1 ? '' : ' ';
+ var separator = startpos === 1 ? '' : '';
+
+ var fullStuff = this.getText();
+ var val = fullStuff.substring(0, startpos);
+// val = val.replace(this.expression, separator + this.options.token + replacement);
+ val = val.replace(this.expression, separator + replacement);
+
+ var posfix = fullStuff.substring(startpos, fullStuff.length);
+ var separator2 = posfix.match(/^\s/) ? '' : ' ';
+
+ var finalFight = val + separator2 + posfix;
+ console.log('222', finalFight)
+ this.setText(finalFight);
+ this.$element.setCursorPosition(val.length + 1);
+ };
+
+ Plugin.prototype.hightlightItem = function () {
+ this.$itemList.find(".-sew-list-item").removeClass("selected");
+
+ var container = this.$itemList.find(".-sew-list-item").parent();
+ var element = this.filtered[this.index].element.addClass("selected");
+
+ var scrollPosition = element.position().top;
+ container.scrollTop(container.scrollTop() + scrollPosition);
+ };
+
+ Plugin.prototype.renderElements = function (values) {
+ $("body").append(this.$itemList);
+
+ var container = this.$itemList.find('ul').empty();
+ values.forEach($.proxy(function (e, i) {
+ var $item = $(Plugin.ITEM_TEMPLATE);
+
+ this.options.elementFactory($item, e);
+
+ e.element = $item.appendTo(container).bind('click', $.proxy(this.onItemClick, this, e)).bind('mouseover', $.proxy(this.onItemHover, this, i));
+ }, this));
+
+ this.index = 0;
+ this.hightlightItem();
+ };
+
+ Plugin.prototype.displayList = function () {
+ if(!this.filtered.length) return;
+
+ this.$itemList.show();
+ var element = this.$element;
+ var offset = this.$element.offset();
+ var pos = element.getCaretPosition();
+
+ this.$itemList.css({
+ left: offset.left + pos.left,
+ top: offset.top + pos.top
+ });
+ };
+
+ Plugin.prototype.hideList = function () {
+ this.$itemList.hide();
+ this.reset();
+ };
+
+ Plugin.prototype.filterList = function (val) {
+ if(val == this.lastFilter) return;
+
+ this.lastFilter = val;
+ this.$itemList.find(".-sew-list-item").remove();
+ var values = this.options.values;
+
+
+ var vals = this.filtered = values.filter($.proxy(function (e) {
+ var exp = new RegExp('\\W*' + this.options.token + e.val + '(\\W|$)');
+ if(!this.options.repeat && this.getText().match(exp)) {
+ return false;
+ }
+
+ return val === "" ||
+ e.val.toLowerCase().indexOf(val.toLowerCase()) >= 0 ||
+ (e.meta || "").toLowerCase().indexOf(val.toLowerCase()) >= 0;
+ }, this));
+
+ if(vals.length) {
+ this.renderElements(vals);
+ this.$itemList.show();
+ } else {
+ this.hideList();
+ }
+ };
+
+ Plugin.getUniqueElements = function (elements) {
+ var target = [];
+
+ elements.forEach(function (e) {
+ var hasElement = target.map(function (j) { return j.val; }).indexOf(e.val) >= 0;
+ if(hasElement) return;
+ target.push(e);
+ });
+
+ return target;
+ };
+
+ Plugin.prototype.getText = function () {
+ return(this.$element.val() || this.$element.text());
+ };
+
+ Plugin.prototype.setText = function (text) {
+ if(this.$element.prop('tagName').match(/input|textarea/i)) {
+ this.$element.val(text);
+ } else {
+ // poors man sanitization
+ text = $("").text(text).html().replace(/\s/g, ' ');
+ this.$element.html(text);
+ }
+ };
+
+ Plugin.prototype.onKeyUp = function (e) {
+ var startpos = this.$element.getCursorPosition();
+ var val = this.getText().substring(0, startpos);
+ var matches = val.match(this.expression);
+
+ if(!matches && this.matched) {
+ this.matched = false;
+ this.dontFilter = false;
+ this.hideList();
+ return;
+ }
+
+ if(matches && !this.matched) {
+ this.displayList();
+ this.lastFilter = "\n";
+ this.matched = true;
+ }
+
+ if(matches && !this.dontFilter) {
+ this.filterList(matches[1]);
+ }
+ };
+
+ Plugin.prototype.onKeyDown = function (e) {
+ var listVisible = this.$itemList.is(":visible");
+ if(!listVisible || (Plugin.KEYS.indexOf(e.keyCode) < 0)) return;
+
+ switch(e.keyCode) {
+ case 9:
+ case 13:
+ this.select();
+ break;
+ case 40:
+ this.next();
+ break;
+ case 38:
+ this.prev();
+ break;
+ case 27:
+ this.$itemList.hide();
+ this.dontFilter = true;
+ break;
+ }
+
+ e.preventDefault();
+ };
+
+ Plugin.prototype.onItemClick = function (element, e) {
+ if(this.cleanupHandle) window.clearTimeout(this.cleanupHandle);
+
+ this.replace(element.val);
+ this.hideList();
+ };
+
+ Plugin.prototype.onItemHover = function (index, e) {
+ this.index = index;
+ this.hightlightItem();
+ };
+
+ $.fn[pluginName] = function (options) {
+ return this.each(function () {
+ if(!$.data(this, 'plugin_' + pluginName)) {
+ $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
+ }
+ });
+ };
+}(jQuery, window));
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index ad3fd7fc2..d8485743f 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -9,6 +9,7 @@
*= require ./jquery.noty.css
*= require ./jquery.tagsinput.css
*= require ./noty_theme_twitter.css
+ *= require ./sew.css
*= require ./zzz.css
*
*= require_tree ./custom/
diff --git a/app/assets/stylesheets/sew.css b/app/assets/stylesheets/sew.css
new file mode 100644
index 000000000..e17db25f7
--- /dev/null
+++ b/app/assets/stylesheets/sew.css
@@ -0,0 +1,37 @@
+.-sew-list-container {
+ background: white;
+ border: 1px solid #DDD;
+ border-radius: 3px;
+ box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
+ min-width: 180px;
+}
+
+.-sew-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 100px;
+ overflow: scroll;
+}
+
+.-sew-list-item {
+ display: block;
+ padding: 5px 10px;
+ border-bottom: 1px solid #DDD;
+ cursor: pointer;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 400px;
+}
+
+
+.-sew-list-item small {
+ color: #afafaf;
+}
+
+.-sew-list-item.selected {
+ color: white;
+ background: #4183C4;
+ text-decoration: none;
+}