Source: ko-extends.js

/* 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;
        }
    };
});