(function (Q, $, window, document, undefined) {
/**
* Q Tools
* @module Q-tools
* @main Q-tools
*/
/**
* Inplace text editor tool
* @class Q inplace
* @constructor
* @param {Object} [options] This is an object of parameters for this function
* @param {String} options.action Required url of the action to issue the request to on save.
* @param {String} [options.method='put'] The HTTP verb to use.
* @param {String} [options.type='textarea'] The type of the input field. Can be "textarea", "text", or "select"
* @param {String} [options.options={}] If the type is "select", then this would be an object of {value: optionTitle} pairs
* @param {Boolean=true} [options.editing] Whether to start out in editing mode
* @param {Boolean=true} [options.editOnClick] Whether to enter editing mode when clicking on the text.
* @param {Boolean} [options.selectOnEdit=true] Whether to select everything in the input field when entering edit mode.
* @param {Boolean=true} [options.showEditButtons=false] Set to true to force showing the edit buttons on touchscreens
* @param {Number} [options.maxWidth=null] The maximum width that the field can grow to
* @param {Number} [options.minWidth=100] The minimum width that the field can shrink to
* @param {String} [options.staticHtml] The static HTML to start out with
* @param {String} [options.placeholder=null] Text to show in the staticHtml or input field when the editor is empty
* @param {Object} [options.template] Can be used to override info for the tool's view template.
* @param {String} [options.template.dir='{{Q}}/views']
* @param {String} [options.template.name='Q/inplace/tool']
* @param {Q.Event} [options.beforeSave] This event triggers before save
* @param {Q.Event} [options.onSave] This event triggers after save
* @param {Q.Event} [options.onCancel] This event triggers after canceling
*/
Q.Tool.define("Q/inplace", function (options) {
var tool = this;
var state = tool.state;
var $te = $(tool.element);
var container = tool.$('.Q_inplace_tool_container');
if (container.length) {
return _Q_inplace_tool_constructor.call(tool, this.element, state);
}
Q.addStylesheet('{{Q}}/css/inplace.css');
// if activated with JS should have following options:
// - action: required. the form action to save tool value
// - name: required. the name for input field
// - method: request method, defaults to 'PUT'
// - type: type of the input - 'text' or 'textarea', defaults to 'textarea'
var o = state;
if (!o || !o.action) {
return console.error("Q/inplace tool: missing option 'action'", o);
}
if (!o.field) {
return console.error("Q/inplace tool: missing option 'field'", o);
}
var staticHtml = o.staticHtml || $te.html();
var staticClass = o.type === 'textarea'
? 'Q_inplace_tool_blockstatic'
: 'Q_inplace_tool_static';
if (o.type !== 'textarea' && o.type !== 'text' && o.type !== 'select') {
throw new Q.Exception("Q/inplace: type must be textarea, text or select");
}
Q.Template.render(
'Q/inplace/tool',
{
'classes': function () {
return o.editing ? 'Q_editing Q_nocancel' : '';
},
staticClass: staticClass,
staticHtml: staticHtml
|| '<span class="Q_placeholder">'
+state.placeholder.encodeHTML()
+'</div>',
method: o.method || 'put',
action: o.action,
field: o.field,
isText: (o.type === 'text'),
isTextarea: (o.type === 'textarea'),
isSelect: (o.type === 'select'),
options: o.options,
placeholder: state.placeholder,
text: staticHtml.decodeHTML(),
type: o.type || 'text'
},
function (err, html) {
if (!html) return;
$te.html(html);
return _Q_inplace_tool_constructor.call(tool, this.element, state, staticHtml);
},
o.template
);
},
{
method: 'put',
type: 'textarea',
editOnClick: true,
selectOnEdit: true,
showEditButtons: false,
maxWidth: null,
minWidth: '.Q_placeholder',
placeholder: 'Type something...',
cancelPrompt: "Would you like to save your changes?",
template: {
dir: '{{Q}}/views',
name: 'Q/inplace/tool'
},
timing: {
waitingInterval: 100,
},
onLoad: new Q.Event(),
beforeSave: new Q.Event(),
onSave: new Q.Event(),
onCancel: new Q.Event()
},
{
/**
* Hide Q/actions, if any
* @method hideActions
*/
hideActions: function () { // Temporarily hide Q/actions if any
this.actionsContainer = $('.Q_actions_container');
this.actionsContainerVisibility = this.actionsContainer.css('visibility');
this.actionsContainer.css('visibility', 'hidden');
},
/**
* Restore Q/actions, if any
* @method restoreActions
*/
restoreActions: function () { // Restore Q/actions if any
if (!this.actionsContainer) return;
this.actionsContainer.css('visibility', this.actionsContainerVisibility);
delete this.actionsContainer;
delete this.actionsContainerVisibility;
}
}
);
function _setSelRange(inputEl, selStart, selEnd) {
if ('setSelectionRange' in inputEl) {
inputEl.focus();
inputEl.setSelectionRange(selStart, selEnd);
} else if (inputEl.createTextRange) {
var range = inputEl.createTextRange();
range.collapse(true);
range.moveEnd('character', selEnd);
range.moveStart('character', selStart);
range.select();
}
}
function _Q_inplace_tool_constructor(element, options, staticHtml) {
// constructor & private declarations
var tool = this;
var state = tool.state;
var blurring = false;
var focusedOn = null;
var dialogMode = false;
var previousValue = null;
var noCancel = false;
var $te = $(tool.element);
var changedMaxWidth, changedMaxHeight;
var container_span = tool.$container = tool.$('.Q_inplace_tool_container');
var static_span = tool.$static = tool.$(
'.Q_inplace_tool_static, .Q_inplace_tool_blockstatic'
).eq(0);
function _waitUntilVisible() {
if (tool.removed) return;
if (!$te.is(':visible')) {
return setTimeout(_waitUntilVisible, state.timing.waitingInterval);
}
tool.$('.Q_inplace_tool_editbuttons').css({
'margin-top': static_span.outerHeight() + 'px',
'line-height': '1px'
});
fieldinput.plugin('Q/placeholders', function () {
fieldinput.plugin('Q/autogrow', {
maxWidth: state.maxWidth || maxWidth,
minWidth: state.minWidth || 0
}, _adjustButtonLayout);
});
_sizing();
}
var edit_button = tool.$edit = tool.$('button.Q_inplace_tool_edit');
var save_button = tool.$save = tool.$('button.Q_inplace_tool_save');
var cancel_button = tool.$cancel = tool.$('button.Q_inplace_tool_cancel');
var fieldinput = tool.$input = tool.$(':input[type!=hidden]').not('button').eq(0)
.addClass('Q_inplace_tool_fieldinput');
var undermessage = tool.$('.Q_inplace_tool_undermessage');
var throbber_img = $('<img />')
.attr('src', Q.url('{{Q}}/img/throbbers/bars16.gif'));
if (container_span.hasClass('Q_nocancel')) {
noCancel = true;
}
Q.onLayout($te[0]).set(_waitUntilVisible, tool);
_waitUntilVisible();
if (state.type === 'select') {
fieldinput.val(staticHtml.decodeHTML());
}
if (staticHtml && state.editOnClick) {
static_span.attr('title', state.placeholder);
}
previousValue = fieldinput.val();
var maxWidth = state.maxWidth;
if (!maxWidth) {
$te.parents().each(function () {
var $this = $(this);
var display = $this.css('display');
if (display === 'block' || display === 'table-cell'
|| (display === 'inline-block' && this.style.width)) {
maxWidth = this;
return false;
}
});
}
setTimeout(function () {
state.onLoad.handle();
}, 0); // hopefully it will be inserted into the DOM by then
function _sizing() {
fieldinput.css({
fontSize: static_span.css('fontSize'),
fontFamily: static_span.css('fontFamily'),
fontWeight: static_span.css('fontWeight'),
letterSpacing: static_span.css('letterSpacing'),
textAlign: static_span.css('textAlign')
});
if (!fieldinput.data('inplace')) {
fieldinput.data('inplace', {});
}
if (container_span.hasClass('Q_editing')) {
fieldinput.data('inplace').widthWasAdjusted = true;
fieldinput.data('inplace').heightWasAdjusted = true;
}
if (fieldinput.is('textarea') && !fieldinput.val()) {
var height = static_span.outerHeight() + 'px';
fieldinput.add(static_span).css('min-height', height);
}
}
this.handleClick = function(event) {
_sizing();
var field_width = static_span.outerWidth();
var field_height = static_span.outerHeight();
changedMaxWidth = changedMaxHeight = false;
if (container_span.css('max-width') === 'none') {
container_span.css('max-width', container_span.width());
changedMaxWidth = true;
}
if (container_span.css('max-height') === 'none') {
container_span.css('max-height', container_span.height());
changedMaxHeight = true;
}
container_span.addClass('Q_editing');
container_span.addClass('Q_discouragePointerEvents');
_beganEditing(container_span);
if (state.bringToFront) {
var $bringToFront = $(state.bringToFront);
var pos = $bringToFront.css('position');
$bringToFront.data(_stateKey_zIndex, $bringToFront.css('zIndex'))
.data(_stateKey_position, pos)
.css({
zIndex: 99999,
position: (pos === 'static') ? 'relative' : pos
});
}
fieldinput.plugin('Q/placeholders', {}, function () {
this.plugin('Q/autogrow', {
maxWidth: state.maxWidth || maxWidth,
minWidth: state.minWidth || 0,
onResize: {"Q/inplace": _adjustButtonLayout}
});
});
if (fieldinput.is('select')) {
field_width += 40;
} else if (fieldinput.is('input[type=text]')) {
field_width += 5;
field_height = static_span.css('line-height');
} else if (fieldinput.is('textarea')) {
field_height = Math.max(field_height, 10);
}
fieldinput.css({
fontSize: static_span.css('fontSize'),
fontFamily: static_span.css('fontFamily'),
fontWeight: static_span.css('fontWeight'),
letterSpacing: static_span.css('letterSpacing'),
width: field_width + 'px'
});
tool.hideActions();
previousValue = fieldinput.val();
if (!fieldinput.is('select')) {
fieldinput.data('inplace').widthWasAdjusted = true;
try {
fieldinput.data('inplace').heightWasAdjusted = true;
} catch (e) {
}
fieldinput.trigger('autogrowCheck');
}
_updateSaveButton();
undermessage.empty().css('display', 'none').addClass('Q_error');
focusedOn = 'fieldinput';
fieldinput.focus();
var selStart = 0;
if (state.selectOnEdit) {
if (fieldinput.attr('type') == 'text' && fieldinput.select) {
fieldinput.select();
}
} else {
selStart = fieldinput.val().length;
if (fieldinput.attr('type') == 'text') {
var v = fieldinput.val();
fieldinput.val('');
fieldinput.val(v); // put cursor at the end
}
}
if (fieldinput.is('textarea')) {
_setSelRange(
fieldinput[0],
selStart,
fieldinput.val().length
);
}
tool.$('.Q_inplace_tool_buttons').css({
width: container_span.outerWidth() + 'px'
});
event && event.preventDefault && event.preventDefault();
};
function onSave () {
var form = $('.Q_inplace_tool_form', $te);
if (state && state.beforeSave) {
if (false === Q.handle(state.beforeSave, this, [form])) {
return false;
}
}
undermessage.html(throbber_img)
.css('display', 'block')
.removeClass('Q_error');
focusedOn = 'fieldinput';
var method = state.method || (form.length && form.attr('method')) || 'post';
var url = form.attr('action');
var used_placeholder = false;
if (fieldinput.attr('placeholder')
&& fieldinput.val() === fieldinput.attr('placeholder')) {
// this is probably due to a custom placeholder mechanism
// so clear the field, rather than saving the placeholder text
fieldinput.val('');
used_placeholder = true;
}
Q.request(url, ['Q_inplace'], function (err, response) {
if (typeof response !== 'object') {
onSaveErrors("returned data is not an object");
return;
}
if (response.errors && response.errors.length) {
onSaveErrors(response.errors[0].message);
return;
}
function afterLoad(alreadyLoaded) {
if (('scriptLines' in response) && ('Q_inplace' in response.scriptLines)) {
eval(response.scriptLines.Q_inplace);
}
}
if(response.scripts && response.scripts.Q_inplace && response.scripts.Q_inplace.length) {
Q.addScript(response.scripts.Q_inplace, afterLoad);
} else {
afterLoad();
}
onSaveSuccess(response);
}, {
method: method,
fields: Q.queryString(form[0])
});
if (used_placeholder) {
fieldinput.val(fieldinput.attr('placeholder'));
}
};
function onSaveErrors (message) {
alert(message);
fieldinput.focus();
undermessage.css('display', 'none');
_restoreZ();
/*
.html(message)
.css('whiteSpace', 'nowrap')
.css('bottom', (-undermessage.height()-3)+'px');
*/
};
function onSaveSuccess (response) {
var newval = fieldinput.val();
if ('slots' in response) {
if ('Q_inplace' in response.slots) {
newval = response.slots.Q_inplace;
}
}
_restoreZ();
if (newval) {
static_span.html(newval);
} else {
static_span.html('<span class="Q_placeholder">'
+state.placeholder.encodeHTML()+'</span>'
);
}
_adjustButtonLayout();
static_span.attr('title', state.placeholder);
undermessage.empty().css('display', 'none').addClass('Q_error');
tool.restoreActions();
fieldinput.blur();
container_span.removeClass('Q_editing')
.removeClass('Q_nocancel')
.removeClass('Q_discouragePointerEvents');
_finishedEditing(container_span);
_hideEditButtons();
noCancel = false;
Q.handle(state.onSave, tool, [response.slots.Q_inplace]);
};
function onCancel (dontAsk) {
if (noCancel) {
return;
}
_restoreZ();
if (!dontAsk && fieldinput.val() != previousValue) {
dialogMode = true;
var continueEditing = confirm(state.cancelPrompt);
dialogMode = false;
if (continueEditing) {
onSave();
return;
}
}
fieldinput.val(previousValue);
fieldinput.blur();
focusedOn = null;
tool.restoreActions();
container_span.removeClass('Q_editing')
.removeClass('Q_discouragePointerEvents');
_finishedEditing(container_span);
_hideEditButtons();
Q.handle(state.onCancel, tool);
Q.Pointer.cancelClick(null, null, true);
};
function onBlur() {
if (noCancel && fieldinput.val() !== previousValue) {
return onSave();
}
setTimeout(function () {
if (focusedOn
|| dialogMode
|| !container_span.hasClass('Q_editing')
) {
return _restoreZ();
}
onCancel();
}, 100);
};
function _restoreZ()
{
if (changedMaxWidth) {
container_span.css('max-width', 'none');
}
if (changedMaxHeight) {
container_span.css('max-height', 'none');
}
if (!state.bringToFront) return;
var $bringToFront = $(state.bringToFront);
$bringToFront.css('zIndex', $bringToFront.data(_stateKey_zIndex))
.css('position', $bringToFront.data(_stateKey_position))
.removeData(_stateKey_zIndex)
.removeData(_stateKey_position);
}
function _editButtons() {
if (Q.info.isTouchscreen) {
if (!state.editOnClick || state.showEditButtons) {
$('.Q_inplace_tool_editbuttons', $te).css({
'margin-top': static_span.outerHeight() + 'px',
'line-height': '1px',
'display': 'inline'
});
}
} else {
container_span.mouseover(function() {
container_span.addClass('Q_hover');
$('.Q_inplace_tool_editbuttons', $te).css({
'margin-top': static_span.outerHeight() + 'px',
'line-height': '1px',
'display': 'inline'
});
}).mouseout(function () {
$('.Q_inplace_tool_editbuttons', $te).css({
'display': 'none'
});
});
}
}
function _hideEditButtons() {
if (!Q.info.isTouchscreen) {
$('.Q_inplace_tool_editbuttons', container_span).css('display', 'none');
}
}
function _adjustButtonLayout() {
var $placeholder = fieldinput.closest('.Q_placeholders_container')
.find('.Q_placeholder');
var $element = fieldinput.is(":visible")
? fieldinput
: ($placeholder.is(":visible") ? $placeholder : static_span);
var margin = $element.outerHeight() + parseInt($element.css('margin-top'));
tool.$('.Q_inplace_tool_editbuttons').css('margin-top', margin+'px');
}
_editButtons();
container_span.mouseout(function() {
container_span.removeClass('Q_hover');
});
container_span.on([Q.Pointer.fastclick, '.Q_inplace'], function (event) {
if ((state.editOnClick && event.target === static_span[0])
|| $(event.target).is('button')) {
Q.Pointer.cancelClick(event, null, true);
Q.Pointer.ended();
event.stopPropagation();
}
});
edit_button.on(Q.Pointer.start, function (event) {
Q.Pointer.cancelClick(event, null, true);
});
if (this.state.editOnClick) {
// happens despite canceled click
static_span.on([Q.Pointer.fastclick, '.Q_inplace'], this.handleClick);
} else {
$te.addClass('Q_inplace_noEditOnClick');
}
edit_button.on(Q.Pointer.start, this.handleClick); // happens despite canceled click
cancel_button.on(Q.Pointer.start, function() {
onCancel(true);
return false;
});
cancel_button.add(save_button).add(edit_button).on(Q.Pointer.end, function() {
return false;
});
cancel_button.on('focus '+Q.Pointer.start.eventName, function() {
setTimeout(function() {
focusedOn = 'cancel_button';
}, 50);
});
cancel_button.blur(function() {
focusedOn = null;
setTimeout(onBlur, 100);
});
save_button.on(Q.Pointer.fastclick, function() { onSave(); return false; });
save_button.on('focus '+Q.Pointer.start.eventName, function() {
setTimeout(function() {
focusedOn = 'save_button';
}, 50);
});
save_button.blur(function() {
focusedOn = null;
setTimeout(onBlur, 100);
});
fieldinput.on('keyup input', _updateSaveButton);
fieldinput.focus(function() {
focusedOn = 'fieldinput';
});
fieldinput.blur(function() {
focusedOn = null;
setTimeout(onBlur, 100);
});
fieldinput.keydown(function _Q_inplace_keydown(e) {
if (!focusedOn) {
return false;
}
var kc = (window.event) ? (event.keyCode || event.which) : e.keyCode;
var sk = (window.event) ? event.shiftKey : e.shiftKey;
if (kc == 13) {
if (! fieldinput.is('textarea')) {
onSave(); return false;
}
} else if (kc == 27) {
onCancel();
return false;
} else if (kc == 9) {
var tags = 'input,textarea,select,.Q_inplace_tool';
var $elements = $(tags).not('.Q_inplace_tool :input');
var $input = $elements.eq($elements.index(tool.element) + (sk ? -1 : 1));
if (!$input.hasClass('Q_inplace_tool')) {
$input.plugin('Q/clickfocus');
} else {
Q.Tool.from($input[0], 'Q/inplace').handleClick();
}
}
});
fieldinput.closest('form').submit(function () {
onSave();
});
fieldinput.on(Q.Pointer.end, function (event) {
Q.Pointer.cancelClick(event, null, true);
Q.Pointer.ended();
event.stopPropagation();
});
fieldinput.click(function (event) {
event.stopPropagation();
});
function _updateSaveButton() {
save_button.css('display', (fieldinput.val() == previousValue)
? 'none'
: 'inline'
);
}
}
function _beganEditing(container_span) {
container_span.parents().each(function () {
var $this = $(this);
$this.data(_stateKey_editing, $this.hasClass('Q_editing'))
.addClass('Q_editing');
});
}
function _finishedEditing(container_span) {
container_span.parents().each(function () {
var $this = $(this);
if (!$this.data(_stateKey_editing)) {
$this.removeClass('Q_editing');
}
});
}
var _stateKey_zIndex = 'Q/inplace zIndex';
var _stateKey_position = 'Q/inplace position';
var _stateKey_editing = 'Q/inplace Q_editing';
Q.Template.set('Q/inplace/tool',
"<div class='Q_inplace_tool_container {{classes}}' style='position: relative;'>"
+ "<div class='Q_inplace_tool_editbuttons'>"
+ "<button class='Q_inplace_tool_edit basic16 basic16_edit'>Edit</button>"
+ "</div>"
+ "<form class='Q_inplace_tool_form' method='{{method}}' action='{{action}}'>"
+ "{{#if isTextarea}}"
+ "<textarea name='{{field}}' placeholder='{{placeholder}}' rows='5' cols='80'>{{text}}</textarea>"
+ "{{/if}}"
+ "{{#if isText}}"
+ "<input name='{{field}}' placeholder='{{placeholder}}' value='{{text}}' type='{{type}}' >"
+ "{{/if}}"
+ "{{#if isSelect}}"
+ "<select name='{{field}}'>"
+ "{{#each options}}"
+ "<option value='{{@key}}'>{{this}}</option>"
+ "{{/each}}"
+ "</select>"
+ "{{/if}}"
+ "<div class='Q_inplace_tool_buttons'>"
+ "<button class='Q_inplace_tool_cancel basic16 basic16_cancel'>Cancel</button>"
+ "<button class='Q_inplace_tool_save basic16 basic16_save'>Save</button>"
+ "</div>"
+ "</form>"
+ "<div class='Q_inplace_tool_static_container {{staticClass}}'>{{& staticHtml}}</div>"
+"</div>"
);
})(Q, jQuery, window, document);