/* global app */
'use strict';
define('ko-extends', ['jquery', 'ko'], function($, ko) {
/**
* Custom binding for autocomplete.
* To enable an autocomplete on a text input, apply the attribute ko-autocomplete like this :
* <input type="text" ko-autocomplete="{source : 'url|data',
* search : 'searchProperty',
* label : 'labelProperty',
* change : callbackFunction,
* delay : 400}" />
* - source : If a string is given, the data will be searched on the URL given,
* else the data object will be used
* - search (default 'label'): If source is an object, 'search' defines the property
* name the autocomplete will be computed on
* - label (default 'label'): This defines the property that will be displayed in the result list
* - change (default null) : This function will be called when the value of the input changes.
* The selected line will be injected as first argument
* - delay (default 400) : This property defines the delay after the last key pressed
* before computing the autocomplete search
*
* @module ko-autocomplete
*/
ko.bindingHandlers.autocomplete = (function() {
/**
* Magic keys
* @type {Object}
*/
var keyCode = {
UP: 38,
DOWN: 40,
LEFT: 37,
RIGHT: 39,
ENTER: 13,
ESCAPE: 27,
TAB: 9
};
var Autocomplete = function() {};
Autocomplete.prototype.init = function(element, valueAccessor, allBindings, viewModel) {
var parameters = ko.unwrap(valueAccessor()),
options = {
search : parameters.search || 'label',
label : parameters.label || 'label',
source : parameters.source,
change : parameters.change,
delay : parameters.delay || 400
};
options.value = parameters.value || options.label;
if (!options.source) {
return;
}
/**
* Initiate the model that will manage the autocomplete results
* @type {Object}
*/
var model = {
result : ko.observableArray([]),
selectedItem : ko.observable(null),
overItem : ko.observable(null)
};
/**
* Go to the previous element in the result list
*/
model.previous = function() {
var index = (this.overItem() ? this.result().indexOf(this.overItem()) : 0) - 1;
if (index < 0) {
index = this.result().length - 1;
}
this.overItem(this.result()[index]);
}.bind(model);
/**
* Got to the next element in the result list
*/
model.next = function() {
var index = ((this.overItem() ? this.result().indexOf(this.overItem()) : 0) + 1) % this.result().length;
this.overItem(this.result()[index]);
}.bind(model);
/**
* Select an item in the result list
* @param {Object} data The selected element
*/
model.select = function(data) {
this.selectedItem(data);
// Reset the results list
this.result([]);
// Affect element data
element.autocompleteData = data;
element.value = data[options.value];
element.blur();
}.bind(model);
/**
* Initialize element
* @type {String}
*/
element.autocomplete = 'off';
$(element)
.wrap('<div class="ko-autocomplete"></div')
.after(
'<div class="ko-autocomplete-result">' +
'<ul ko-foreach="result" ko-visible="!!result().length">' +
'<li ko-attr="{value: $data.value}" ' +
'ko-html="label" ' +
'ko-event="{mousedown : $parent.select.bind($parent)}" ' +
'ko-class="{hover : $parent.overItem() == $data}"></li>' +
'</ul>' +
'</div>'
);
/**
* The timeout for compute filter delay
*/
var ajaxTimeout;
/**
* Listen on the element events
*/
$(element).on({
/*
* Display the result when the data changes
*/
input : function() {
element.autocompleteData = null;
var value = element.value;
if (!value) {
model.result([]);
model.selectedItem(null);
return;
}
// Search on an array
if (ko.isObservable(options.source) || options.source instanceof Array) {
var source = ko.isObservable(options.source) ? options.source() : options.source;
// Filter the source by the
var filters = ko.utils.arrayFilter(source, function(item) {
return item[options.search].match(element.value);
});
// Change the output items to match to the autocomplete parameters
var displayed = ko.utils.arrayMap(filters, function(item) {
item.label = item[options.label];
item.value = item[options.value];
return item;
});
// Display the result to the user
model.result(displayed);
}
else if (typeof options.source === 'string') {
clearTimeout(ajaxTimeout);
ajaxTimeout = setTimeout(function() {
// Load the result by AJAX request.
// In this case, search, value, and label parameters are not used
$.ajax({
url : options.source,
type : 'get',
dataType : 'json',
data : {
q : element.value
}
})
.done(function(data) {
model.result(data);
if (!data.length) {
model.selectedItem(null);
}
});
}, options.delay);
}
},
/*
* Blur the input
*/
blur : function() {
if (!this.autocompleteData || this.autocompleteData !== model.selectedItem()) {
model.selectedItem(null);
this.autocompleteData = null;
}
model.result([]);
if (options.change) {
options.change.apply(viewModel, [this.autocompleteData]);
}
},
/*
* Navigate in the result list
* @param {Event} event The keydown event
*/
keydown : function(event) {
if (model.result().length) {
switch (event.keyCode) {
// Move up
case keyCode.UP :
model.previous();
break;
// Move down
case keyCode.DOWN :
model.next();
break;
// Tab key
case keyCode.TAB :
if (event.shiftKey) {
// Go preivous element
model.previous();
}
else {
model.next();
}
break;
// Select item
case keyCode.ENTER :
if (!model.overItem()) {
return true;
}
model.select(model.overItem());
break;
// Hide the result list
case keyCode.ESCAPE :
model.result([]);
break;
default :
return true;
}
return false;
}
return true;
}
});
/**
* Apply the model to the node
*/
ko.applyBindings(model, $(element).next('.ko-autocomplete-result').get(0));
};
return new Autocomplete();
})();
/**
* Rename the binding css to class
*
* @module ko-class
*/
ko.bindingHandlers.class = ko.bindingHandlers.css;
/**
* Custom binding for Ace. This applies the code editor ace on the text area
* <textarea ko-ace="{theme : 'aceTheme', language : 'php', readonly : true, change : callbackFunction}"></textarea>
* - theme (default 'chrome') : The ace theme
* - language (mandatory): The programming language to use
* - readonly (default false) : If set to true, the editor will only highlight the code
* - chnage : Callback when the value of the editor changes
*
* @see https://ace.c9.io/#nav=about
* @module ko-ace
*/
ko.bindingHandlers.ace = {
update : function(element, valueAccessor) {
require(['ace'], function(ace) {
var parameters = ko.unwrap(valueAccessor());
ace.config.set('modePath', app.baseUrl + 'ext/ace/');
ace.config.set('workerPath', app.baseUrl + 'ext/ace/');
ace.config.set('themePath', app.baseUrl + 'ext/ace/');
var editor = ace.edit(element.id);
editor.setTheme('ace/theme/' + (parameters.theme || 'chrome'));
editor.getSession().setMode('ace/mode/' + parameters.language);
editor.setShowPrintMargin(false);
editor.setReadOnly(parameters.readonly || false);
editor.getSession().on('change', function() {
var value = editor.getValue();
if (parameters.change) {
parameters.change(value);
}
});
});
}
};
/**
* Custom binding for CKEditor
*
* <textarea ko-wysiwyg></textarea>
*
* @module ko-wysiwyg
* @see http://ckeditor.com/
*/
ko.bindingHandlers.wysiwyg = {
update : function(element) {
require(['ckeditor'], function(CKEDITOR) {
if (!CKEDITOR.dom.element.get(element.id).getEditor()) {
var editor = CKEDITOR.replace(element.id, {
language : app.language,
removeButtons : 'Save,Scayt,Rtl,Ltr,Language,Flash',
entities : false,
on : {
change : function(event) {
$('#' + element.id).val(event.editor.getData()).trigger('change');
}
}
});
if (document.getElementById('theme-base-stylesheet')) {
editor.addContentsCss(document.getElementById('theme-base-stylesheet').href);
}
}
});
}
};
/**
* Extend the knockout syntax to allow devs to write ko-{bind}="value" as tag attribute
*
* @param {NodeElement} node The node to process knockout on
*/
ko.bindingProvider.instance.preprocessNode = function(node) {
var dataBind = node.dataset && node.dataset.bind || '';
for (var name in ko.bindingHandlers) {
if (ko.bindingHandlers.hasOwnProperty(name)) {
var attrName = 'ko-' + name;
if (node.getAttribute && node.getAttribute(attrName)) {
dataBind += (dataBind ? ',' : '') + name + ': ' + node.getAttribute(attrName);
}
}
}
if (dataBind) {
node.dataset.bind = dataBind;
}
};
});