Improved text module feature.
This commit is contained in:
parent
72778a2d78
commit
a9e8bef64a
6 changed files with 543 additions and 3 deletions
|
@ -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 )
|
||||
)
|
||||
|
|
|
@ -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 = "<div><span /> <small /></div>"
|
||||
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',
|
||||
|
|
185
app/assets/javascripts/app/lib/base/jquery.caretposition.js
Normal file
185
app/assets/javascripts/app/lib/base/jquery.caretposition.js
Normal file
|
@ -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 : $('<div id="textarea_simulator"/>').css({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}).appendTo(document.body),
|
||||
|
||||
toHtml : function(text) {
|
||||
return text.replace(/</g,'<').replace(/>/g,'>').replace(/\n/g, '<br>')
|
||||
.split(' ').join('<span style="white-space:prev-wrap"> </span>');
|
||||
},
|
||||
// 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 = $('<span class="before"/>').html(cal.toHtml(beforeText)),
|
||||
focus = $('<span class="focus"/>'),
|
||||
after = $('<span class="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
|
||||
});
|
||||
});
|
275
app/assets/javascripts/app/lib/base/jquery.sew.js
Normal file
275
app/assets/javascripts/app/lib/base/jquery.sew.js
Normal file
|
@ -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 = "<div class='-sew-list-container' style='display: none; position: absolute;'><ul class='-sew-list'></ul></div>";
|
||||
|
||||
Plugin.ITEM_TEMPLATE = '<li class="-sew-list-item"></li>';
|
||||
|
||||
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 = $("<span>").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));
|
|
@ -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/
|
||||
|
|
37
app/assets/stylesheets/sew.css
Normal file
37
app/assets/stylesheets/sew.css
Normal file
|
@ -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;
|
||||
}
|
Loading…
Reference in a new issue