<commit_before>//##################################################################################################################
// #STRUCTUREBOARD#
/* global CMS */

(function ($) {
    'use strict';

    // CMS.$ will be passed for $
    $(function () {
        var placeholders = $('.cms-dragarea:not(.cms-clipboard-containers)');
        function actualizeEmptyPlaceholders() {
            placeholders.each(function () {
                var placeholder = $(this);
                if (placeholder
                    .find('> .cms-draggables')
                    .children('.cms-draggable:not(.cms-draggable-is-dragging)').length) {
                    placeholder.removeClass('cms-dragarea-empty');
                } else {
                    placeholder.addClass('cms-dragarea-empty');
                }
            });
        }

        function actualizePluginsCollapsibleStatus(els) {
            els.each(function () {
                var childList = $(this);
                var pluginDragItem = childList.closest('.cms-draggable').find('> .cms-dragitem');

                if (childList.children().length) {
                    pluginDragItem.addClass('cms-dragitem-collapsable cms-dragitem-expanded');
                } else {
                    pluginDragItem.removeClass('cms-dragitem-collapsable');
                }
            });
        }

        /*!
         * StructureBoard
         * handles drag & drop, mode switching and
         */
        CMS.StructureBoard = new CMS.Class({

            implement: [CMS.API.Helpers],

            options: {
                speed: 300
            },

            initialize: function (options) {
                this.options = $.extend(true, {}, this.options, options);
                this.config = CMS.config;
                this.settings = CMS.settings;

                // elements
                this._setupUI();

                // states
                this.click = 'click.cms';
                this.state = false;
                this.dragging = false;

                // setup initial stuff
                this._setup();

                // setup events
                this._events();
                actualizeEmptyPlaceholders();
            },

            _setupUI: function setupUI() {
                var container = $('.cms-structure');
                var toolbar = $('#cms-toolbar');
                this.ui = {
                    container: container,
                    content: $('.cms-structure-content'),
                    doc: $(document),
                    window: $(window),
                    toolbar: toolbar,
                    sortables: $('.cms-draggables'), // global scope to include clipboard
                    plugins: $('.cms-plugin'),
                    render_model: $('.cms-render-model'),
                    placeholders: $('.cms-placeholder'),
                    dragitems: $('.cms-draggable'),
                    dragareas: $('.cms-dragarea'),
                    clipboard: $('.cms-clipboard'),
                    toolbarModeSwitcher: toolbar.find('.cms-toolbar-item-cms-mode-switcher'),
                    toolbarModeLinks: toolbar.find('.cms-toolbar-item-cms-mode-switcher a')
                };
            },

            // initial methods
            _setup: function () {
                var that = this;
                // cancel if there are no dragareas
                if (!this.ui.dragareas.length) {
                    return false;
                }

                // cancel if there is no structure / content switcher
                if (!this.ui.toolbarModeSwitcher.length) {
                    return false;
                }

                // setup toolbar mode
                // FIXME this setTimeout is needed because
                // plugins are initialized after all the scripts are processed
                // which should be fixed btw. _resizeBoard wants plugins to be initialized,
                // otherwise throws errors
                setTimeout(function () {
                    if (that.settings.mode === 'structure') {
                        that.show(true);
                    } else {
                        // triggering hide here to switch proper classnames on switcher
                        that.hide(true);
                    }
                }, 0);

                // check if modes should be visible
                if (this.ui.placeholders.length) {
                    this.ui.toolbarModeSwitcher.show();
                }

                // add drag & drop functionality
                this._drag();
            },

            _events: function () {
                var that = this;
                var modes = that.ui.toolbarModeLinks;

                // show edit mode
                modes.eq(1).on(that.click, function (e) {
                    e.preventDefault();
                    // cancel if already active
                    if (that.settings.mode === 'edit') {
                        return false;
                    }
                    // otherwise hide
                    that.hide();
                });
                // show structure mode
                modes.eq(0).on(that.click, function (e) {
                    e.preventDefault();
                    // cancel if already active
                    if (that.settings.mode === 'structure') {
                        return false;
                    }
                    // otherwise show
                    that.show();
                });

                // keyboard handling
                // only if there is a structure / content switcher
                if (that.ui.toolbarModeSwitcher.length) {
                    that.ui.doc.on('keydown', function (e) {
                        // check if we have an important focus
                        var haveFocusedField = document.activeElement !== document.body;
                        if (e.keyCode === CMS.KEYS.SPACE && !haveFocusedField) {
                            e.preventDefault();
                            if (that.settings.mode === 'structure') {
                                that.hide();
                            } else if (that.settings.mode === 'edit') {
                                that.show();
                            }
                        }
                    });
                }
            },

            // public methods
            show: function (init) {
                // cancel show if live modus is active
                if (CMS.config.mode === 'live') {
                    return false;
                }

                // set active item
                var modes = this.ui.toolbarModeLinks;
                modes.removeClass('cms-btn-active').eq(0).addClass('cms-btn-active');

                // show clipboard
                this.ui.clipboard.fadeIn(this.options.speed);

                // apply new settings
                this.settings.mode = 'structure';
                if (!init) {
                    this.settings = this.setSettings(this.settings);
                }

                // ensure all elements are visible
                this.ui.dragareas.show();

                // show canvas
                this._showBoard();
            },

            hide: function (init) {
                // cancel show if live modus is active
                if (CMS.config.mode === 'live') {
                    return false;
                }

                // set active item
                var modes = this.ui.toolbarModeLinks;
                modes.removeClass('cms-btn-active').eq(1).addClass('cms-btn-active');

                // hide clipboard if in edit mode
                this.ui.container.find('.cms-clipboard').hide();

                // hide clipboard
                this.ui.clipboard.hide();

                this.settings.mode = 'edit';
                if (!init) {
                    this.settings = this.setSettings(this.settings);
                }

                // hide canvas
                this._hideBoard();
            },

            /**
             * gets the id of the element
             *
             * @param el jQuery element to get id from
             * @return String
             */
            getId: function (el) {
                // cancel if no element is defined
                if (el === undefined || el === null || el.length <= 0) {
                    return false;
                }

                var id = null;
                var cls = el.attr('class').split(' ')[1];

                if (el.hasClass('cms-plugin')) {
                    id = cls.replace('cms-plugin-', '');
                } else if (el.hasClass('cms-draggable')) {
                    id = cls.replace('cms-draggable-', '');
                } else if (el.hasClass('cms-placeholder')) {
                    id = cls.replace('cms-placeholder-', '');
                } else if (el.hasClass('cms-dragbar')) {
                    id = cls.replace('cms-dragbar-', '');
                } else if (el.hasClass('cms-dragarea')) {
                    id = cls.replace('cms-dragarea-', '');
                }

                return id;
            },

            getIds: function (els) {
                var that = this;
                var array = [];
                els.each(function () {
                    array.push(that.getId($(this)));
                });
                return array;
            },

            // private methods
            _showBoard: function () {
                var that = this;

                // show container
                this.ui.container.show();
                this.ui.dragareas.css('opacity', 1);

                this.ui.plugins.not(this.ui.render_model).hide();
                this.ui.placeholders.show();

                // attach event
                if (CMS.config.simpleStructureBoard) {
                    var content = this.ui.content;
                    var areas = content.find('.cms-dragarea');
                    // set correct css attributes for the new mode
                    content.addClass('cms-structure-content-simple');
                    areas.addClass('cms-dragarea-simple');
                    // lets reorder placeholders
                    areas.each(function (index, item) {
                        if ($(item).hasClass('cms-dragarea-static')) {
                            content.append(item);
                        }
                    });
                    // now lets get the first instance and add some padding
                    areas.filter('.cms-dragarea-static').eq(0).css('margin-top', '50px');
                } else {
                    this.ui.container.addClass('cms-structure-dynamic');
                    this.ui.window.on('resize.sideframe', function () {
                        that._resizeBoard();
                    }).trigger('resize.sideframe');
                }
            },

            _hideBoard: function () {
                // hide elements
                this.ui.container.hide();
                this.ui.plugins.show();
                this.ui.placeholders.hide();

                // detach event
                this.ui.window.off('resize.sideframe');

                this.ui.window.trigger('structureboard_hidden.sideframe');
                if (!CMS.config.simpleStructureBoard) {
                    this.ui.container.height(this.ui.doc.outerHeight());
                }
            },

            /*
             * @deprecated as of CMS 3.2
             */
            _resizeBoard: function () {
                // calculate placeholder position
                var id = null;
                var area = null;
                var min = null;
                var areaParentOffset = null;
                var that = this;

                // have to delay since height changes when toggling modes
                setTimeout(function () {
                    that.ui.container.height(that.ui.doc.outerHeight());
                }, 0);

                // start calculating
                this.ui.placeholders.each(function (index, item) {
                    item = $(item);
                    id = item.data('settings').placeholder_id;
                    area = $('.cms-dragarea-' + id);
                    // to calculate the correct offset, we need to set the
                    // placeholders correct heights and than set the according position
                    item.height(area.outerHeight(true));
                    // set min width
                    min = (item.width()) ? 0 : 150;
                    // as area is "css positioned" and jquery offset function is relative to the
                    // document (not the first relative/absolute parent) we need to substract
                    // first relative/absolute parent offset.
                    areaParentOffset = $(area).offsetParent().offset();
                    area.css({
                        top: item.offset().top - areaParentOffset.top - 5,
                        left: item.offset().left - areaParentOffset.left - min,
                        width: item.width() + min
                    });
                });
            },

            _drag: function () {
                var that = this;
                var originalPluginContainer;

                this.ui.sortables.nestedSortable({
                    items: '> .cms-draggable:not(.cms-draggable-disabled .cms-draggable)',
                    placeholder: 'cms-droppable',
                    connectWith: '.cms-draggables:not(.cms-hidden)',
                    tolerance: 'intersect',
                    toleranceElement: '> div',
                    dropOnEmpty: true,
                    // cloning huge structure is a performance loss compared to cloning just a dragitem
                    helper: function createHelper(e, item) {
                        var clone = item.find('> .cms-dragitem').clone();
                        clone.wrap('

');
                        return clone.parent();
                    },
                    appendTo: '.cms-structure-content',
                    // appendTo: '#cms-toolbar',
                    cursor: 'move',
                    cursorAt: { left: -15, top: -15 },
                    opacity: 1,
                    zIndex: 9999999,
                    delay: 100,
                    tabSize: 15,
                    // nestedSortable
                    listType: 'div.cms-draggables',
                    doNotClear: true,
                    disableNestingClass: 'cms-draggable-disabled',
                    errorClass: 'cms-draggable-disallowed',
                    start: function (e, ui) {
                        originalPluginContainer = ui.item.closest('.cms-draggables');
                        that.dragging = true;
                        // show empty
                        actualizeEmptyPlaceholders();
                        // ensure all menus are closed
                        CMS.Plugin._hideSettingsMenu();
                        // keep in mind that caching cms-draggables query only works
                        // as long as we don't create them on the fly
                        that.ui.sortables.each(function () {
                            var element = $(this);
                            if (element.children().length === 0) {
                                element.removeClass('cms-hidden');
                            }
                        });

                        // fixes placeholder height
                        ui.item.addClass('cms-is-dragging');
                        ui.helper.addClass('cms-draggable-is-dragging');
                        if (ui.item.find('> .cms-draggables').children().length) {
                            ui.helper.addClass('cms-draggable-stack');
                        }

                        // attach escape event to cancel dragging
                        that.ui.doc.on('keyup.cms.interrupt', function (e, cancel) {
                            if (e.keyCode === CMS.KEYS.ESC && that.dragging || cancel) {
                                that.state = false;
                                $.ui.sortable.prototype._mouseStop();
                                that.ui.sortables.trigger('mouseup');
                            }
                        });
                    },

                    beforeStop: function (event, ui) {
                        that.dragging = false;
                        ui.item.removeClass('cms-is-dragging cms-draggable-stack');
                        that.ui.doc.off('keyup.cms.interrupt');
                    },

                    update: function (event, ui) {
                        // cancel if isAllowed returns false
                        if (!that.state) {
                            return false;
                        }

                        var newPluginContainer = ui.item.closest('.cms-draggables');
                        if (!originalPluginContainer.is(newPluginContainer)) {
                            actualizePluginsCollapsibleStatus(newPluginContainer.add(originalPluginContainer));
                        } else {
                            // if we moved inside same container,
                            // but event is fired on a parent, discard update
                            if (!newPluginContainer.is(this)) {
                                return false;
                            }
                        }

                        // we pass the id to the updater which checks within the backend the correct place
                        var id = that.getId(ui.item);
                        var plugin = $('.cms-plugin-' + id);

                        // check if we copy/paste a plugin or not
                        if (plugin.closest('.cms-clipboard').length) {
                            plugin.trigger('cms.plugin.update');
                        } else {
                            plugin.trigger('cms.plugins.update');
                        }

                        // reset placeholder without entries
                        that.ui.sortables.each(function () {
                            var element = $(this);
                            if (element.children().length === 0) {
                                element.addClass('cms-hidden');
                            }
                        });

                        actualizeEmptyPlaceholders();
                    },
                    isAllowed: function (placeholder, placeholderParent, originalItem) {
                        // cancel if action is executed
                        if (CMS.API.locked) {
                            return false;
                        }
                        // getting restriction array
                        var bounds = [];
                        // save original state events
                        var original = $('.cms-plugin-' + that.getId(originalItem));
                        // cancel if item has no settings
                        if (original.length === 0 || original.data('settings') === null) {
                            return false;
                        }
                        var type = original.data('settings').plugin_type;
                        // prepare variables for bound
                        var holderId = that.getId(placeholder.closest('.cms-dragarea'));
                        var holder = $('.cms-placeholder-' + holderId);
                        var plugin;
                        if (placeholderParent && placeholderParent.length) {
                            // placeholderParent is always latest, it maybe that
                            // isAllowed is called _before_ placeholder is moved to a child plugin
                            plugin = $('.cms-plugin-' + that.getId(placeholderParent.closest('.cms-draggable')));
                        } else {
                            plugin = $('.cms-plugin-' + that.getId(placeholder.closest('.cms-draggable')));
                        }

                        // now set the correct bounds
                        if (holder.length) {
                            bounds = holder.data('settings').plugin_restriction;
                        }
                        if (plugin.length) {
                            bounds = plugin.data('settings').plugin_restriction;
                        }

                        // if parent has class disabled, dissalow drop
                        if (placeholder.parent().hasClass('cms-draggable-disabled')) {
                            return false;
                        }

                        // if restrictions is still empty, proceed
                        that.state = (!bounds.length || $.inArray(type, bounds) !== -1) ? true : false;

                        return that.state;
                    }
                });
            }

        });
    });
})(CMS.$);
<commit_msg>comments<commit_after>//##################################################################################################################
// #STRUCTUREBOARD#
/* global CMS */

(function ($) {
    'use strict';

    // CMS.$ will be passed for $
    $(function () {
        var placeholders = $('.cms-dragarea:not(.cms-clipboard-containers)');
        function actualizeEmptyPlaceholders() {
            placeholders.each(function () {
                var placeholder = $(this);
                if (placeholder
                    .find('> .cms-draggables')
                    .children('.cms-draggable:not(.cms-draggable-is-dragging)').length) {
                    placeholder.removeClass('cms-dragarea-empty');
                } else {
                    placeholder.addClass('cms-dragarea-empty');
                }
            });
        }

        function actualizePluginsCollapsibleStatus(els) {
            els.each(function () {
                var childList = $(this);
                var pluginDragItem = childList.closest('.cms-draggable').find('> .cms-dragitem');

                if (childList.children().length) {
                    pluginDragItem.addClass('cms-dragitem-collapsable cms-dragitem-expanded');
                } else {
                    pluginDragItem.removeClass('cms-dragitem-collapsable');
                }
            });
        }

        /**
         * StructureBoard
         * handles drag & drop, mode switching and
         */
        CMS.StructureBoard = new CMS.Class({

            implement: [CMS.API.Helpers],

            options: {
                speed: 300
            },

            initialize: function (options) {
                this.options = $.extend(true, {}, this.options, options);
                this.config = CMS.config;
                this.settings = CMS.settings;

                // elements
                this._setupUI();

                // states
                this.click = 'click.cms';
                this.state = false;
                this.dragging = false;

                // setup initial stuff
                this._setup();

                // setup events
                this._events();
                actualizeEmptyPlaceholders();
            },

            _setupUI: function setupUI() {
                var container = $('.cms-structure');
                var toolbar = $('#cms-toolbar');
                this.ui = {
                    container: container,
                    content: $('.cms-structure-content'),
                    doc: $(document),
                    window: $(window),
                    toolbar: toolbar,
                    sortables: $('.cms-draggables'), // global scope to include clipboard
                    plugins: $('.cms-plugin'),
                    render_model: $('.cms-render-model'),
                    placeholders: $('.cms-placeholder'),
                    dragitems: $('.cms-draggable'),
                    dragareas: $('.cms-dragarea'),
                    clipboard: $('.cms-clipboard'),
                    toolbarModeSwitcher: toolbar.find('.cms-toolbar-item-cms-mode-switcher'),
                    toolbarModeLinks: toolbar.find('.cms-toolbar-item-cms-mode-switcher a')
                };
            },

            // initial methods
            _setup: function () {
                var that = this;
                // cancel if there are no dragareas
                if (!this.ui.dragareas.length) {
                    return false;
                }

                // cancel if there is no structure / content switcher
                if (!this.ui.toolbarModeSwitcher.length) {
                    return false;
                }

                // setup toolbar mode
                // FIXME this setTimeout is needed because
                // plugins are initialized after all the scripts are processed
                // which should be fixed btw. _resizeBoard wants plugins to be initialized,
                // otherwise throws errors
                setTimeout(function () {
                    if (that.settings.mode === 'structure') {
                        that.show(true);
                    } else {
                        // triggering hide here to switch proper classnames on switcher
                        that.hide(true);
                    }
                }, 0);

                // check if modes should be visible
                if (this.ui.placeholders.length) {
                    this.ui.toolbarModeSwitcher.show();
                }

                // add drag & drop functionality
                this._drag();
            },

            _events: function () {
                var that = this;
                var modes = that.ui.toolbarModeLinks;

                // show edit mode
                modes.eq(1).on(that.click, function (e) {
                    e.preventDefault();
                    // cancel if already active
                    if (that.settings.mode === 'edit') {
                        return false;
                    }
                    // otherwise hide
                    that.hide();
                });
                // show structure mode
                modes.eq(0).on(that.click, function (e) {
                    e.preventDefault();
                    // cancel if already active
                    if (that.settings.mode === 'structure') {
                        return false;
                    }
                    // otherwise show
                    that.show();
                });

                // keyboard handling
                // only if there is a structure / content switcher
                if (that.ui.toolbarModeSwitcher.length) {
                    that.ui.doc.on('keydown', function (e) {
                        // check if we have an important focus
                        var haveFocusedField = document.activeElement !== document.body;
                        if (e.keyCode === CMS.KEYS.SPACE && !haveFocusedField) {
                            e.preventDefault();
                            if (that.settings.mode === 'structure') {
                                that.hide();
                            } else if (that.settings.mode === 'edit') {
                                that.show();
                            }
                        }
                    });
                }
            },

            // public methods
            show: function (init) {
                // cancel show if live modus is active
                if (CMS.config.mode === 'live') {
                    return false;
                }

                // set active item
                var modes = this.ui.toolbarModeLinks;
                modes.removeClass('cms-btn-active').eq(0).addClass('cms-btn-active');

                // show clipboard
                this.ui.clipboard.fadeIn(this.options.speed);

                // apply new settings
                this.settings.mode = 'structure';
                if (!init) {
                    this.settings = this.setSettings(this.settings);
                }

                // ensure all elements are visible
                this.ui.dragareas.show();

                // show canvas
                this._showBoard();
            },

            hide: function (init) {
                // cancel show if live modus is active
                if (CMS.config.mode === 'live') {
                    return false;
                }

                // set active item
                var modes = this.ui.toolbarModeLinks;
                modes.removeClass('cms-btn-active').eq(1).addClass('cms-btn-active');

                // hide clipboard if in edit mode
                this.ui.container.find('.cms-clipboard').hide();

                // hide clipboard
                this.ui.clipboard.hide();

                this.settings.mode = 'edit';
                if (!init) {
                    this.settings = this.setSettings(this.settings);
                }

                // hide canvas
                this._hideBoard();
            },

            /**
             * gets the id of the element
             *
             * @param el jQuery element to get id from
             * @return String
             */
            getId: function (el) {
                // cancel if no element is defined
                if (el === undefined || el === null || el.length <= 0) {
                    return false;
                }

                var id = null;
                var cls = el.attr('class').split(' ')[1];

                if (el.hasClass('cms-plugin')) {
                    id = cls.replace('cms-plugin-', '');
                } else if (el.hasClass('cms-draggable')) {
                    id = cls.replace('cms-draggable-', '');
                } else if (el.hasClass('cms-placeholder')) {
                    id = cls.replace('cms-placeholder-', '');
                } else if (el.hasClass('cms-dragbar')) {
                    id = cls.replace('cms-dragbar-', '');
                } else if (el.hasClass('cms-dragarea')) {
                    id = cls.replace('cms-dragarea-', '');
                }

                return id;
            },

            /**
             * gets the ids of the list of  elements
             *
             * @param el jQuery elements to get id from
             * @return {String[]}
             */
            getIds: function (els) {
                var that = this;
                var array = [];
                els.each(function () {
                    array.push(that.getId($(this)));
                });
                return array;
            },

            // private methods
            _showBoard: function () {
                var that = this;

                // show container
                this.ui.container.show();
                this.ui.dragareas.css('opacity', 1);

                this.ui.plugins.not(this.ui.render_model).hide();
                this.ui.placeholders.show();

                // attach event
                if (CMS.config.simpleStructureBoard) {
                    var content = this.ui.content;
                    var areas = content.find('.cms-dragarea');
                    // set correct css attributes for the new mode
                    content.addClass('cms-structure-content-simple');
                    areas.addClass('cms-dragarea-simple');
                    // lets reorder placeholders
                    areas.each(function (index, item) {
                        if ($(item).hasClass('cms-dragarea-static')) {
                            content.append(item);
                        }
                    });
                    // now lets get the first instance and add some padding
                    areas.filter('.cms-dragarea-static').eq(0).css('margin-top', '50px');
                } else {
                    this.ui.container.addClass('cms-structure-dynamic');
                    this.ui.window.on('resize.sideframe', function () {
                        that._resizeBoard();
                    }).trigger('resize.sideframe');
                }
            },

            _hideBoard: function () {
                // hide elements
                this.ui.container.hide();
                this.ui.plugins.show();
                this.ui.placeholders.hide();

                // detach event
                this.ui.window.off('resize.sideframe');

                this.ui.window.trigger('structureboard_hidden.sideframe');
                if (!CMS.config.simpleStructureBoard) {
                    this.ui.container.height(this.ui.doc.outerHeight());
                }
            },

            /*
             * @deprecated as of CMS 3.2
             */
            _resizeBoard: function () {
                // calculate placeholder position
                var id = null;
                var area = null;
                var min = null;
                var areaParentOffset = null;
                var that = this;

                // have to delay since height changes when toggling modes
                setTimeout(function () {
                    that.ui.container.height(that.ui.doc.outerHeight());
                }, 0);

                // start calculating
                this.ui.placeholders.each(function (index, item) {
                    item = $(item);
                    id = item.data('settings').placeholder_id;
                    area = $('.cms-dragarea-' + id);
                    // to calculate the correct offset, we need to set the
                    // placeholders correct heights and than set the according position
                    item.height(area.outerHeight(true));
                    // set min width
                    min = (item.width()) ? 0 : 150;
                    // as area is "css positioned" and jquery offset function is relative to the
                    // document (not the first relative/absolute parent) we need to substract
                    // first relative/absolute parent offset.
                    areaParentOffset = $(area).offsetParent().offset();
                    area.css({
                        top: item.offset().top - areaParentOffset.top - 5,
                        left: item.offset().left - areaParentOffset.left - min,
                        width: item.width() + min
                    });
                });
            },

            _drag: function () {
                var that = this;
                var originalPluginContainer;

                this.ui.sortables.nestedSortable({
                    items: '> .cms-draggable:not(.cms-draggable-disabled .cms-draggable)',
                    placeholder: 'cms-droppable',
                    connectWith: '.cms-draggables:not(.cms-hidden)',
                    tolerance: 'intersect',
                    toleranceElement: '> div',
                    dropOnEmpty: true,
                    // cloning huge structure is a performance loss compared to cloning just a dragitem
                    helper: function createHelper(e, item) {
                        var clone = item.find('> .cms-dragitem').clone();
                        clone.wrap('

');
                        return clone.parent();
                    },
                    appendTo: '.cms-structure-content',
                    // appendTo: '#cms-toolbar',
                    cursor: 'move',
                    cursorAt: { left: -15, top: -15 },
                    opacity: 1,
                    zIndex: 9999999,
                    delay: 100,
                    tabSize: 15,
                    // nestedSortable
                    listType: 'div.cms-draggables',
                    doNotClear: true,
                    disableNestingClass: 'cms-draggable-disabled',
                    errorClass: 'cms-draggable-disallowed',
                    start: function (e, ui) {
                        originalPluginContainer = ui.item.closest('.cms-draggables');
                        that.dragging = true;
                        // show empty
                        actualizeEmptyPlaceholders();
                        // ensure all menus are closed
                        CMS.Plugin._hideSettingsMenu();
                        // keep in mind that caching cms-draggables query only works
                        // as long as we don't create them on the fly
                        that.ui.sortables.each(function () {
                            var element = $(this);
                            if (element.children().length === 0) {
                                element.removeClass('cms-hidden');
                            }
                        });

                        // fixes placeholder height
                        ui.item.addClass('cms-is-dragging');
                        ui.helper.addClass('cms-draggable-is-dragging');
                        if (ui.item.find('> .cms-draggables').children().length) {
                            ui.helper.addClass('cms-draggable-stack');
                        }

                        // attach escape event to cancel dragging
                        that.ui.doc.on('keyup.cms.interrupt', function (e, cancel) {
                            if (e.keyCode === CMS.KEYS.ESC && that.dragging || cancel) {
                                that.state = false;
                                $.ui.sortable.prototype._mouseStop();
                                that.ui.sortables.trigger('mouseup');
                            }
                        });
                    },

                    beforeStop: function (event, ui) {
                        that.dragging = false;
                        ui.item.removeClass('cms-is-dragging cms-draggable-stack');
                        that.ui.doc.off('keyup.cms.interrupt');
                    },

                    update: function (event, ui) {
                        // cancel if isAllowed returns false
                        if (!that.state) {
                            return false;
                        }

                        var newPluginContainer = ui.item.closest('.cms-draggables');
                        if (!originalPluginContainer.is(newPluginContainer)) {
                            actualizePluginsCollapsibleStatus(newPluginContainer.add(originalPluginContainer));
                        } else {
                            // if we moved inside same container,
                            // but event is fired on a parent, discard update
                            if (!newPluginContainer.is(this)) {
                                return false;
                            }
                        }

                        // we pass the id to the updater which checks within the backend the correct place
                        var id = that.getId(ui.item);
                        var plugin = $('.cms-plugin-' + id);

                        // check if we copy/paste a plugin or not
                        if (plugin.closest('.cms-clipboard').length) {
                            plugin.trigger('cms.plugin.update');
                        } else {
                            plugin.trigger('cms.plugins.update');
                        }

                        // reset placeholder without entries
                        that.ui.sortables.each(function () {
                            var element = $(this);
                            if (element.children().length === 0) {
                                element.addClass('cms-hidden');
                            }
                        });

                        actualizeEmptyPlaceholders();
                    },
                    isAllowed: function (placeholder, placeholderParent, originalItem) {
                        // cancel if action is executed
                        if (CMS.API.locked) {
                            return false;
                        }
                        // getting restriction array
                        var bounds = [];
                        // save original state events
                        var original = $('.cms-plugin-' + that.getId(originalItem));
                        // cancel if item has no settings
                        if (original.length === 0 || original.data('settings') === null) {
                            return false;
                        }
                        var type = original.data('settings').plugin_type;
                        // prepare variables for bound
                        var holderId = that.getId(placeholder.closest('.cms-dragarea'));
                        var holder = $('.cms-placeholder-' + holderId);
                        var plugin;
                        if (placeholderParent && placeholderParent.length) {
                            // placeholderParent is always latest, it maybe that
                            // isAllowed is called _before_ placeholder is moved to a child plugin
                            plugin = $('.cms-plugin-' + that.getId(placeholderParent.closest('.cms-draggable')));
                        } else {
                            plugin = $('.cms-plugin-' + that.getId(placeholder.closest('.cms-draggable')));
                        }

                        // now set the correct bounds
                        if (holder.length) {
                            bounds = holder.data('settings').plugin_restriction;
                        }
                        if (plugin.length) {
                            bounds = plugin.data('settings').plugin_restriction;
                        }

                        // if parent has class disabled, dissalow drop
                        if (placeholder.parent().hasClass('cms-draggable-disabled')) {
                            return false;
                        }

                        // if restrictions is still empty, proceed
                        that.state = (!bounds.length || $.inArray(type, bounds) !== -1) ? true : false;

                        return that.state;
                    }
                });
            }

        });
    });
})(CMS.$);
<|endoftext|>


<commit_before>/**
	_moon.CaptionDecorator_ wraps a control with a caption. The position of the
	caption is defined via the _side_ property.

		{kind: "moon.CaptionDecorator", side: "top", content: "Top Label", components: [
			{kind: "moon.Button", content: "My Button", ontap: "buttonTapped"},
		]}
*/
enyo.kind({
	name: "moon.CaptionDecorator",
	handlers: {
		onSpotlightFocus: "spotFocus",
		onSpotlightBlur:  "spotBlur"
	},
	published: {
		side: "top",
		showOnFocus: false
	},
	//* @protected
	captionPositioned: false,
	clientBounds: null,
	captionBounds: null,
	decoratorBounds: null,
	classes: "moon-button-caption-decorator",
	components: [
		{kind: "enyo.Control", name: "leftCaption",     classes: "moon-caption left",   canGenerate: false, content: "Left Caption"},
		{kind: "enyo.Control", name: "topCaption",      classes: "moon-caption top",    canGenerate: false, content: "Top Caption"},
		{kind: "enyo.Control", name: "client",          classes: "moon-caption-client"},
		{kind: "enyo.Control", name: "rightCaption",    classes: "moon-caption right",  canGenerate: false, content: "Right Caption"},
		{kind: "enyo.Control", name: "bottomCaption",   classes: "moon-caption bottom", canGenerate: false, content: "Bottom Caption"}
	],
	create: function() {
		this.inherited(arguments);
		this.sideChanged();
		this.showOnFocusChanged();
	},
	//* If _this.showOnFocus_ is _true_, reset caption position on reflow
	reflow: function() {
		this.inherited(arguments);

		if (this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},

	// Change handlers

	sideChanged: function() {
		var side = this.getSide();

		this.$.topCaption.canGenerate =     (side === "top");
		this.$.rightCaption.canGenerate =   (side === "right");
		this.$.bottomCaption.canGenerate =  (side === "bottom");
		this.$.leftCaption.canGenerate =    (side === "left");

		// If this control has already been rendered, re-render to update caption side
		if (this.hasNode()) {
			// If _showOnFocus_ is _true_, reset caption position
			if (this.getShowOnFocus()) {
				this.resetCaptionPosition();
			}
			// Re-render to display caption on proper side
			this.render();
		}
	},
	showOnFocusChanged: function() {
		this.addRemoveClass("showOnFocus", this.getShowOnFocus());

		// If _showOnFocus_ is _true_, reset caption position
		if (this.hasNode() && this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},
	contentChanged: function() {
		this.$[this.getSide()+"Caption"].setContent(this.getContent());

		// If _showOnFocus_ is _true_, reset caption position
		if (this.hasNode() && this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},

	// Event handlers

	//* Add spotlight class when button is focused, and calculate caption position if required
	spotFocus: function () {
		this.addClass("spotlight");

		if (this.hasNode() && this.getShowOnFocus()) {
			this.positionCaption();
		}
	},
	//* Remove spotlight class when button is blurred
	spotBlur: function() {
		this.removeClass("spotlight");
	},

	// Caption positioning

	//* Return current caption control
	getCaptionControl: function() {
		return this.$[this.getSide()+"Caption"];
	},
	//* Reset cached position values and reposition caption if currently spotted
	resetCaptionPosition: function() {
		this.resetCachedBounds();
		this.captionPositioned = false;

		if (this.hasNode() && this.hasClass("spotlight")) {
			this.positionCaption();
		}
	},
	//* Position caption based on the value of _this.side_
	positionCaption: function() {
		if (this.captionPositioned) {
			return;
		}

		var bounds = this.getDecoratorBounds(),
			clientBounds = this.getClientBounds(),
			captionBounds = this.getCaptionBounds();

		switch (this.getSide()) {
		case "left":
			this.centerCaptionVertically(bounds, captionBounds);
			this.positionCaptionAtLeftEdge(bounds, clientBounds, captionBounds);
			break;
		case "right":
			this.centerCaptionVertically(bounds, captionBounds);
			this.positionCaptionAtRightEdge(bounds, clientBounds, captionBounds);
			break;
		case "top":
			this.centerCaptionHorizontally(bounds, captionBounds);
			this.positionCaptionAtTopEdge(bounds, clientBounds, captionBounds);
			break;
		case "bottom":
			this.centerCaptionHorizontally(bounds, captionBounds);
			this.positionCaptionAtBottomEdge(bounds, clientBounds, captionBounds);
			break;
		}

		this.captionPositioned = true;
	},
	//* Center caption control vertically relative to _this.decoratorBounds.height_
	centerCaptionVertically: function(inBounds, inCaptionBounds) {
		this.getCaptionControl().applyStyle("top", ((inBounds.height - inCaptionBounds.height)/2) + "px");
	},
	//* Center caption control horizontally relative to _this.decoratorBounds.width_
	centerCaptionHorizontally: function(inBounds, inCaptionBounds) {
		this.getCaptionControl().applyStyle("left", ((inBounds.width - inCaptionBounds.width)/2) + "px");
	},
	//* Position caption at left edge of _this.$.client_
	positionCaptionAtLeftEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = (-1 * inCaptionBounds.width) + ((inBounds.width - inClientBounds.width)/2) - inCaptionBounds.marginRight;
		this.getCaptionControl().applyStyle("left", position + "px");
	},
	//* Position caption at right edge of _this.$.client_
	positionCaptionAtRightEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = inBounds.width - ((inBounds.width - inClientBounds.width)/2) + inCaptionBounds.marginLeft;
		this.getCaptionControl().applyStyle("left", position + "px");
	},
	//* Position caption at top edge of _this.$.client_
	positionCaptionAtTopEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = (-1 * this.getCaptionBounds().height) + ((inBounds.height - inClientBounds.height)/2) - inCaptionBounds.marginBottom;
		this.getCaptionControl().applyStyle("top", position + "px");
	},
	//* Position caption at bottom edge of _this.$.client_
	positionCaptionAtBottomEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = inBounds.height - ((inBounds.height - inClientBounds.height)/2) + inCaptionBounds.marginTop;
		this.getCaptionControl().applyStyle("top", position + "px");
	},
	//* Cache result from _this.getBounds()_ call and save in _this.decoratorBounds_
	getDecoratorBounds: function() {
		this.decoratorBounds = this.decoratorBounds || this.getBounds();
		return this.decoratorBounds;
	},
	//* Cache caption bounds and save in _this.captionBounds_
	getCaptionBounds: function() {
		this.captionBounds = this.captionBounds || enyo.mixin(this.getCaptionControl().getBounds(), this.getCaptionMarginBounds());
		return this.captionBounds;
	},
	//* Cache client bounds and save in _this.clientBounds_
	getClientBounds: function() {
		this.clientBounds = this.clientBounds || this.$.client.getBounds();
		return this.clientBounds;
	},
	//* Clear cached bounds
	resetCachedBounds: function() {
		this.clientBounds = null;
		this.captionBounds = null;
		this.decoratorBounds = null;
	},
	//* Return margins of caption control
	getCaptionMarginBounds: function() {
		var margins = enyo.dom.calcMarginExtents(this.getCaptionControl().hasNode());
		return {
			marginTop:      margins.top,
			marginRight:    margins.right,
			marginBottom:   margins.bottom,
			marginLeft:     margins.left
		};
	}
});<commit_msg>GF-12212: Update not to add marginLeft/Top value when positioning at right/bottom edge.<commit_after>/**
	_moon.CaptionDecorator_ wraps a control with a caption. The position of the
	caption is defined via the _side_ property.

		{kind: "moon.CaptionDecorator", side: "top", content: "Top Label", components: [
			{kind: "moon.Button", content: "My Button", ontap: "buttonTapped"},
		]}
*/
enyo.kind({
	name: "moon.CaptionDecorator",
	handlers: {
		onSpotlightFocus: "spotFocus",
		onSpotlightBlur:  "spotBlur"
	},
	published: {
		side: "top",
		showOnFocus: false
	},
	//* @protected
	captionPositioned: false,
	clientBounds: null,
	captionBounds: null,
	decoratorBounds: null,
	classes: "moon-button-caption-decorator",
	components: [
		{kind: "enyo.Control", name: "leftCaption",     classes: "moon-caption left",   canGenerate: false, content: "Left Caption"},
		{kind: "enyo.Control", name: "topCaption",      classes: "moon-caption top",    canGenerate: false, content: "Top Caption"},
		{kind: "enyo.Control", name: "client",          classes: "moon-caption-client"},
		{kind: "enyo.Control", name: "rightCaption",    classes: "moon-caption right",  canGenerate: false, content: "Right Caption"},
		{kind: "enyo.Control", name: "bottomCaption",   classes: "moon-caption bottom", canGenerate: false, content: "Bottom Caption"}
	],
	create: function() {
		this.inherited(arguments);
		this.sideChanged();
		this.showOnFocusChanged();
	},
	//* If _this.showOnFocus_ is _true_, reset caption position on reflow
	reflow: function() {
		this.inherited(arguments);

		if (this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},

	// Change handlers

	sideChanged: function() {
		var side = this.getSide();

		this.$.topCaption.canGenerate =     (side === "top");
		this.$.rightCaption.canGenerate =   (side === "right");
		this.$.bottomCaption.canGenerate =  (side === "bottom");
		this.$.leftCaption.canGenerate =    (side === "left");

		// If this control has already been rendered, re-render to update caption side
		if (this.hasNode()) {
			// If _showOnFocus_ is _true_, reset caption position
			if (this.getShowOnFocus()) {
				this.resetCaptionPosition();
			}
			// Re-render to display caption on proper side
			this.render();
		}
	},
	showOnFocusChanged: function() {
		this.addRemoveClass("showOnFocus", this.getShowOnFocus());

		// If _showOnFocus_ is _true_, reset caption position
		if (this.hasNode() && this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},
	contentChanged: function() {
		this.$[this.getSide()+"Caption"].setContent(this.getContent());

		// If _showOnFocus_ is _true_, reset caption position
		if (this.hasNode() && this.getShowOnFocus()) {
			this.resetCaptionPosition();
		}
	},

	// Event handlers

	//* Add spotlight class when button is focused, and calculate caption position if required
	spotFocus: function () {
		this.addClass("spotlight");

		if (this.hasNode() && this.getShowOnFocus()) {
			this.positionCaption();
		}
	},
	//* Remove spotlight class when button is blurred
	spotBlur: function() {
		this.removeClass("spotlight");
	},

	// Caption positioning

	//* Return current caption control
	getCaptionControl: function() {
		return this.$[this.getSide()+"Caption"];
	},
	//* Reset cached position values and reposition caption if currently spotted
	resetCaptionPosition: function() {
		this.resetCachedBounds();
		this.captionPositioned = false;

		if (this.hasNode() && this.hasClass("spotlight")) {
			this.positionCaption();
		}
	},
	//* Position caption based on the value of _this.side_
	positionCaption: function() {
		if (this.captionPositioned) {
			return;
		}

		var bounds = this.getDecoratorBounds(),
			clientBounds = this.getClientBounds(),
			captionBounds = this.getCaptionBounds();

		switch (this.getSide()) {
		case "left":
			this.centerCaptionVertically(bounds, captionBounds);
			this.positionCaptionAtLeftEdge(bounds, clientBounds, captionBounds);
			break;
		case "right":
			this.centerCaptionVertically(bounds, captionBounds);
			this.positionCaptionAtRightEdge(bounds, clientBounds, captionBounds);
			break;
		case "top":
			this.centerCaptionHorizontally(bounds, captionBounds);
			this.positionCaptionAtTopEdge(bounds, clientBounds, captionBounds);
			break;
		case "bottom":
			this.centerCaptionHorizontally(bounds, captionBounds);
			this.positionCaptionAtBottomEdge(bounds, clientBounds, captionBounds);
			break;
		}

		this.captionPositioned = true;
	},
	//* Center caption control vertically relative to _this.decoratorBounds.height_
	centerCaptionVertically: function(inBounds, inCaptionBounds) {
		this.getCaptionControl().applyStyle("top", ((inBounds.height - inCaptionBounds.height)/2) + "px");
	},
	//* Center caption control horizontally relative to _this.decoratorBounds.width_
	centerCaptionHorizontally: function(inBounds, inCaptionBounds) {
		this.getCaptionControl().applyStyle("left", ((inBounds.width - inCaptionBounds.width)/2) + "px");
	},
	//* Position caption at left edge of _this.$.client_
	positionCaptionAtLeftEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = (-1 * inCaptionBounds.width) + ((inBounds.width - inClientBounds.width)/2) - inCaptionBounds.marginRight;
		this.getCaptionControl().applyStyle("left", position + "px");
	},
	//* Position caption at right edge of _this.$.client_
	positionCaptionAtRightEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = inBounds.width - ((inBounds.width - inClientBounds.width)/2);
		this.getCaptionControl().applyStyle("left", position + "px");
	},
	//* Position caption at top edge of _this.$.client_
	positionCaptionAtTopEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = (-1 * this.getCaptionBounds().height) + ((inBounds.height - inClientBounds.height)/2) - inCaptionBounds.marginBottom;
		this.getCaptionControl().applyStyle("top", position + "px");
	},
	//* Position caption at bottom edge of _this.$.client_
	positionCaptionAtBottomEdge: function(inBounds, inClientBounds, inCaptionBounds) {
		var position = inBounds.height - ((inBounds.height - inClientBounds.height)/2);
		this.getCaptionControl().applyStyle("top", position + "px");
	},
	//* Cache result from _this.getBounds()_ call and save in _this.decoratorBounds_
	getDecoratorBounds: function() {
		this.decoratorBounds = this.decoratorBounds || this.getBounds();
		return this.decoratorBounds;
	},
	//* Cache caption bounds and save in _this.captionBounds_
	getCaptionBounds: function() {
		this.captionBounds = this.captionBounds || enyo.mixin(this.getCaptionControl().getBounds(), this.getCaptionMarginBounds());
		return this.captionBounds;
	},
	//* Cache client bounds and save in _this.clientBounds_
	getClientBounds: function() {
		this.clientBounds = this.clientBounds || this.$.client.getBounds();
		return this.clientBounds;
	},
	//* Clear cached bounds
	resetCachedBounds: function() {
		this.clientBounds = null;
		this.captionBounds = null;
		this.decoratorBounds = null;
	},
	//* Return margins of caption control
	getCaptionMarginBounds: function() {
		var margins = enyo.dom.calcMarginExtents(this.getCaptionControl().hasNode());
		return {
			marginTop:      margins.top,
			marginRight:    margins.right,
			marginBottom:   margins.bottom,
			marginLeft:     margins.left
		};
	}
});<|endoftext|>


<commit_before>frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({
	make_input() {
		this._super();

		this.$input_area.addClass('form-control table-multiselect');
		this.$input.removeClass('form-control');

		this.$input.on("awesomplete-selectcomplete", () => {
			this.$input.val('').focus();
		});

		// used as an internal model to store values
		this.rows = [];
		// used as an internal model to filter awesomplete values
		this._rows_list = [];

		this.$input_area.on('click', (e) => {
			if (e.target === this.$input_area.get(0)) {
				this.$input.focus();
			}
		});

		this.$input_area.on('click', '.btn-remove', (e) => {
			const $target = $(e.currentTarget);
			const $value = $target.closest('.tb-selected-value');

			const value = decodeURIComponent($value.data().value);
			const link_field = this.get_link_field();
			this.rows = this.rows.filter(row => row[link_field.fieldname] !== value);

			this.parse_validate_and_set_in_model('');
		});
		this.$input_area.on('click', '.btn-link-to-form', (e) => {
			const $target = $(e.currentTarget);
			const $value = $target.closest('.tb-selected-value');

			const value = decodeURIComponent($value.data().value);
			const link_field = this.get_link_field();
			frappe.set_route('Form', link_field.options, value);
		});
		this.$input.on('keydown', e => {
			// if backspace key pressed on empty input, delete last value
			if (e.keyCode == frappe.ui.keyCode.BACKSPACE && e.target.value === '') {
				this.rows = this.rows.slice(0, this.rows.length - 1);
				this.parse_validate_and_set_in_model('');
			}
		});
	},
	setup_buttons() {
		this.$input_area.find('.link-btn').remove();
	},
	parse(value) {
		const link_field = this.get_link_field();

		if (value) {
			if (this.frm) {
				const new_row = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname);
				new_row[link_field.fieldname] = value;
				this.rows = this.frm.doc[this.df.fieldname];
			} else {
				this.rows.push({
					[link_field.fieldname]: value
				});
			}
		}
		this._rows_list = this.rows.map(row => row[link_field.fieldname]);
		return this.rows;
	},
	validate(value) {
		const rows = (value || []).slice();

		// validate the value just entered
		if (this.df.ignore_link_validation) {
			return rows;
		}

		const link_field = this.get_link_field();
		if (rows.length === 0) {
			return rows;
		}

		const all_rows_except_last = rows.slice(0, rows.length - 1);
		const last_row = rows[rows.length - 1];

		// validate the last value entered
		const link_value = last_row[link_field.fieldname];

		// falsy value
		if (!link_value) {
			return all_rows_except_last;
		}

		// duplicate value
		if (all_rows_except_last.map(row => row[link_field.fieldname]).includes(link_value)) {
			return all_rows_except_last;
		}

		const validate_promise = this.validate_link_and_fetch(this.df, this.get_options(),
			this.docname, link_value);

		return validate_promise.then(validated_value => {
			if (validated_value === link_value) {
				return rows;
			} else {
				rows.pop();
				return rows;
			}
		});
	},
	set_formatted_input(value) {
		this.rows = value || [];
		const link_field = this.get_link_field();
		const values = this.rows.map(row => row[link_field.fieldname]);
		this.set_pill_html(values);
	},
	set_pill_html(values) {
		const html = values
			.map(value => this.get_pill_html(value))
			.join('');

		this.$input_area.find('.tb-selected-value').remove();
		this.$input_area.prepend(html);
	},
	get_pill_html(value) {
		const encoded_value = encodeURIComponent(value);
		return `
			<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
				${__(value)}
				${frappe.utils.icon('close')}
			</button>
		`;
	},
	get_options() {
		return (this.get_link_field() || {}).options;
	},
	get_link_field() {
		if (!this._link_field) {
			const meta = frappe.get_meta(this.df.options);
			this._link_field = meta.fields.find(df => df.fieldtype === 'Link');
			if (!this._link_field) {
				throw new Error('Table MultiSelect requires a Table with atleast one Link field');
			}
		}
		return this._link_field;
	},
	custom_awesomplete_filter: function(awesomplete) {
		let me = this;

		awesomplete.filter = function(item) {
			if (in_list(me._rows_list, item.value)) {
				return false;
			}

			return true;
		};
	}
});
<commit_msg>fix: Override get_model_value for table multiselect<commit_after>frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({
	make_input() {
		this._super();

		this.$input_area.addClass('form-control table-multiselect');
		this.$input.removeClass('form-control');

		this.$input.on("awesomplete-selectcomplete", () => {
			this.$input.val('').focus();
		});

		// used as an internal model to store values
		this.rows = [];
		// used as an internal model to filter awesomplete values
		this._rows_list = [];

		this.$input_area.on('click', (e) => {
			if (e.target === this.$input_area.get(0)) {
				this.$input.focus();
			}
		});

		this.$input_area.on('click', '.btn-remove', (e) => {
			const $target = $(e.currentTarget);
			const $value = $target.closest('.tb-selected-value');

			const value = decodeURIComponent($value.data().value);
			const link_field = this.get_link_field();
			this.rows = this.rows.filter(row => row[link_field.fieldname] !== value);

			this.parse_validate_and_set_in_model('');
		});
		this.$input_area.on('click', '.btn-link-to-form', (e) => {
			const $target = $(e.currentTarget);
			const $value = $target.closest('.tb-selected-value');

			const value = decodeURIComponent($value.data().value);
			const link_field = this.get_link_field();
			frappe.set_route('Form', link_field.options, value);
		});
		this.$input.on('keydown', e => {
			// if backspace key pressed on empty input, delete last value
			if (e.keyCode == frappe.ui.keyCode.BACKSPACE && e.target.value === '') {
				this.rows = this.rows.slice(0, this.rows.length - 1);
				this.parse_validate_and_set_in_model('');
			}
		});
	},
	setup_buttons() {
		this.$input_area.find('.link-btn').remove();
	},
	parse(value) {
		const link_field = this.get_link_field();

		if (value) {
			if (this.frm) {
				const new_row = frappe.model.add_child(this.frm.doc, this.df.options, this.df.fieldname);
				new_row[link_field.fieldname] = value;
				this.rows = this.frm.doc[this.df.fieldname];
			} else {
				this.rows.push({
					[link_field.fieldname]: value
				});
			}
		}
		this._rows_list = this.rows.map(row => row[link_field.fieldname]);
		return this.rows;
	},
	get_model_value() {
		let value = this._super();
		return value ? value.filter(d => !d.__islocal) : value;
	},
	validate(value) {
		const rows = (value || []).slice();

		// validate the value just entered
		if (this.df.ignore_link_validation) {
			return rows;
		}

		const link_field = this.get_link_field();
		if (rows.length === 0) {
			return rows;
		}

		const all_rows_except_last = rows.slice(0, rows.length - 1);
		const last_row = rows[rows.length - 1];

		// validate the last value entered
		const link_value = last_row[link_field.fieldname];

		// falsy value
		if (!link_value) {
			return all_rows_except_last;
		}

		// duplicate value
		if (all_rows_except_last.map(row => row[link_field.fieldname]).includes(link_value)) {
			return all_rows_except_last;
		}

		const validate_promise = this.validate_link_and_fetch(this.df, this.get_options(),
			this.docname, link_value);

		return validate_promise.then(validated_value => {
			if (validated_value === link_value) {
				return rows;
			} else {
				rows.pop();
				return rows;
			}
		});
	},
	set_formatted_input(value) {
		this.rows = value || [];
		const link_field = this.get_link_field();
		const values = this.rows.map(row => row[link_field.fieldname]);
		this.set_pill_html(values);
	},
	set_pill_html(values) {
		const html = values
			.map(value => this.get_pill_html(value))
			.join('');

		this.$input_area.find('.tb-selected-value').remove();
		this.$input_area.prepend(html);
	},
	get_pill_html(value) {
		const encoded_value = encodeURIComponent(value);
		return `
			<button class="data-pill btn tb-selected-value" data-value="${encoded_value}">
				${__(value)}
				${frappe.utils.icon('close')}
			</button>
		`;
	},
	get_options() {
		return (this.get_link_field() || {}).options;
	},
	get_link_field() {
		if (!this._link_field) {
			const meta = frappe.get_meta(this.df.options);
			this._link_field = meta.fields.find(df => df.fieldtype === 'Link');
			if (!this._link_field) {
				throw new Error('Table MultiSelect requires a Table with atleast one Link field');
			}
		}
		return this._link_field;
	},
	custom_awesomplete_filter: function(awesomplete) {
		let me = this;

		awesomplete.filter = function(item) {
			if (in_list(me._rows_list, item.value)) {
				return false;
			}

			return true;
		};
	}
});
<|endoftext|>


<commit_before>/*
 *  BulletPattern.js
 *  2014/08/02
 *  @auther minimo
 *  This Program is MIT license.
 */
(function() {

tds.bulletPattern = [];
var $ = bulletml.dsl;

$rank = 1;

//弾種
var RS = function(action) { return $.bullet(action, {type: "normal", color:320}); };
var BS = function(action) { return $.bullet(action, {type: "normal", color:240}); };
var RL = function(action) { return $.bullet(action, {type: "large", color:320}); };
var BL = function(action) { return $.bullet(action, {type: "large", color:240}); };

var $interval = function(v) { return $.wait(v) };

//速度
var $spd = function(v) {
    v = v===undefined?1:v;
    return $.speed($rank*0.1*v+v); };

var $fireBS = function(spd) { return $.fire($.direction(0), $.speed(spd) || $spd, BS) };
var $fireRS = function(spd) { return $.fire($.direction(0), $.speed(spd) || $spd, RS) };

//no operation
tds.bulletPattern["nop"] = new bulletml.Root({top: $.action([$.wait(10000)])});

//Basic aim bullet
tds.bulletPattern["basic-aim1"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

tds.bulletPattern["basic-aim2"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

/** 自機狙いn-way弾 */
var $nway = function(way, rangeFrom, rangeTo, speed, bullet, offsetX, offsetY, autonomy) {
    return $.action([
        $.fire($.direction(rangeFrom), speed, bullet || RS, offsetX, offsetY, autonomy),
        $.bindVar("way", "Math.max(2, " + way + ")"),
        $.repeat("$way-1", [
            $.fire($.direction("((" + rangeTo + ")-(" + rangeFrom + "))/($way-1)", "sequence"), speed, bullet || RS, offsetX, offsetY, autonomy),
        ]),
    ]);
};
var $nwayVs = function(way, rangeFrom, rangeTo, bullet, offsetX, offsetY, autonomy) {
    return function(spd) {
        return $nway(way, rangeFrom, rangeTo, spd, bullet, offsetX, offsetY, autonomy);
    };
};

/** 絶対n-way弾 */
var $absoluteNway = function(way, rangeFrom, rangeTo, speed, bullet, offsetX, offsetY) {
    return $.action([
        $.fire($.direction(rangeFrom, "absolute"), speed, bullet || RS, offsetX, offsetY),
        $.bindVar("way", "Math.max(2, " + way + ")"),
        $.repeat("$way-1", [
            $.fire($.direction("((" + rangeTo + ")-(" + rangeFrom + "))/($way-1)", "sequence"), speed, bullet || RNS, offsetX, offsetY),
        ]),
    ]);
};
var $absoluteNwayVs = function(way, rangeFrom, rangeTo, bullet, offsetX, offsetY) {
    return function(spd) {
        return $absoluteNway(way, rangeFrom, rangeTo, spd, bullet, offsetX, offsetY);
    };
};

/**
 * ウィップ
 * @param {bulletml.Speed} baseSpeed 初回のスピード
 * @param {number} delta 2回目以降のスピード増分
 * @param {number} count 回数
 * @param {function(bulletml.Speed):bulletml.Action} スピードを受け取りActionを返す関数
 */
var $whip = function(baseSpeed, delta, count, actionFunc) {
    return $.action([
        actionFunc(baseSpeed),
        $.repeat(count + "-1", [
            actionFunc($.speed(delta, "sequence")),
        ]),
    ]);
};

//cube1
tds.bulletPattern["cube1"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

//cube2
tds.bulletPattern["cube2"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(3, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

//square
tds.bulletPattern["square1"] = new bulletml.Root({
    top: $.action([
        $.wait(180),
        $.repeat(999, [
            $.fire($.direction(-40), $spd(3), RL),
            $.repeat(4, [
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
                $.fire($.direction( 10, "sequence"), $spd(3), RL),
//                $absoluteNway(10, 90, 270, $spd(3), RL, 0, 0),
                $interval(10),
            ]),
            $interval(60),
        ]),
    ]),
});

//square1-child
tds.bulletPattern["square1-child"] = new bulletml.Root({
    top: $.action([
        $.wait(300),
        $.repeat(3, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

})();
<commit_msg>update bullet pattern<commit_after>/*
 *  BulletPattern.js
 *  2014/08/02
 *  @auther minimo
 *  This Program is MIT license.
 */
(function() {

tds.bulletPattern = [];
var $ = bulletml.dsl;

$rank = 1;

//弾種
var RS = function(action) { return $.bullet(action, {type: "normal", color:320}); };
var BS = function(action) { return $.bullet(action, {type: "normal", color:240}); };
var RL = function(action) { return $.bullet(action, {type: "large", color:320}); };
var BL = function(action) { return $.bullet(action, {type: "large", color:240}); };

var $interval = function(v) { return $.wait(v) };

//速度
var $spd = function(v) {
    v = v===undefined?1:v;
    return $.speed($rank*0.1*v+v); };

var $fireBS = function(spd) { return $.fire($.direction(0), $.speed(spd) || $spd, BS) };
var $fireRS = function(spd) { return $.fire($.direction(0), $.speed(spd) || $spd, RS) };

//no operation
tds.bulletPattern["nop"] = new bulletml.Root({top: $.action([$.wait(10000)])});

//Basic aim bullet
tds.bulletPattern["basic-aim1"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

tds.bulletPattern["basic-aim2"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

/** 自機狙いn-way弾 */
var $nway = function(way, rangeFrom, rangeTo, speed, bullet, offsetX, offsetY, autonomy) {
    return $.action([
        $.fire($.direction(rangeFrom), speed, bullet || RS, offsetX, offsetY, autonomy),
        $.bindVar("way", "Math.max(2, " + way + ")"),
        $.repeat("$way-1", [
            $.fire($.direction("((" + rangeTo + ")-(" + rangeFrom + "))/($way-1)", "sequence"), speed, bullet || RS, offsetX, offsetY, autonomy),
        ]),
    ]);
};
var $nwayVs = function(way, rangeFrom, rangeTo, bullet, offsetX, offsetY, autonomy) {
    return function(spd) {
        return $nway(way, rangeFrom, rangeTo, spd, bullet, offsetX, offsetY, autonomy);
    };
};

/** 絶対n-way弾 */
var $absoluteNway = function(way, rangeFrom, rangeTo, speed, bullet, offsetX, offsetY) {
    return $.action([
        $.fire($.direction(rangeFrom, "absolute"), speed, bullet || RS, offsetX, offsetY),
        $.bindVar("way", "Math.max(2, " + way + ")"),
        $.repeat("$way-1", [
            $.fire($.direction("((" + rangeTo + ")-(" + rangeFrom + "))/($way-1)", "sequence"), speed, bullet || RNS, offsetX, offsetY),
        ]),
    ]);
};
var $absoluteNwayVs = function(way, rangeFrom, rangeTo, bullet, offsetX, offsetY) {
    return function(spd) {
        return $absoluteNway(way, rangeFrom, rangeTo, spd, bullet, offsetX, offsetY);
    };
};

/**
 * ウィップ
 * @param {bulletml.Speed} baseSpeed 初回のスピード
 * @param {number} delta 2回目以降のスピード増分
 * @param {number} count 回数
 * @param {function(bulletml.Speed):bulletml.Action} スピードを受け取りActionを返す関数
 */
var $whip = function(baseSpeed, delta, count, actionFunc) {
    return $.action([
        actionFunc(baseSpeed),
        $.repeat(count + "-1", [
            actionFunc($.speed(delta, "sequence")),
        ]),
    ]);
};

//cube1
tds.bulletPattern["cube1"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(999, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

//cube2
tds.bulletPattern["cube2"] = new bulletml.Root({
    top: $.action([
        $.wait(60),
        $.repeat(3, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

//square
tds.bulletPattern["square1"] = new bulletml.Root({
    top1: $.action([
        $.wait(180),
        $.fire($.direction(  0), $spd(3), RL),
        $.repeat(999, [
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $interval(30),
        ]),
    ]),
    top2: $.action([
        $.wait(180),
        $.fire($.direction(120), $spd(3), RL),
        $.repeat(999, [
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $interval(30),
        ]),
    ]),
    top3: $.action([
        $.wait(180),
        $.fire($.direction(240), $spd(3), RL),
        $.repeat(999, [
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $.fire($.direction( 10, "sequence"), $spd(3), RL),
            $interval(30),
        ]),
    ]),
});

//square1-child
tds.bulletPattern["square1-child"] = new bulletml.Root({
    top: $.action([
        $.wait(300),
        $.repeat(3, [
            $fireRS(6),
            $.wait("$rand * 60"),
        ]),
    ]),
});

})();
<|endoftext|>


<commit_before>import { execFile } from 'child_process';
import fs from 'fs';
import path from 'path';

import { initLog } from 'roc';
import flow from 'flow-bin';

import { default as flowconfig } from '../resources/flowconfig';

const log = initLog();
const HEAVY_EXCLAMATION_MARK_SYMBOL = '❗';

const expectedOutput = stdout => /Found \d+ error[s]?/.test(stdout);
const numErrors = (stdout) => /Found (\d+) error[s]?/.exec(stdout)[1];
const removeErrorFooter = (stdout) => stdout.replace(/Found \d+ error[s]?/, '');
const errorString = (num) => ((num > 1) ? 'errors' : 'error');
const configFileExists = () => fs.existsSync(path.join(process.cwd(), '.flowconfig'));

const createConfigFile = () => {
    const target = path.join(process.cwd(), '.flowconfig');

    fs.writeFileSync(target, flowconfig);
};

export default () => () => {
    if (!configFileExists()) {
        log.small.warn('No .flowconfig file found - creating.');
        log.small.info('You should commit .flowconfig to your source code repsository.');
        createConfigFile();
    } else {
        log.small.info('Using .flowconfig file in root of project.');
    }

    log.small.info('Starting Flow type check. This may take a short while, please be patient.\n');

    return () => execFile(flow, ['check'], (err, stdout) => {
        if (err && expectedOutput(stdout)) {
            const num = numErrors(stdout);

            log.small.raw('warn', 'red', HEAVY_EXCLAMATION_MARK_SYMBOL)(`Flow found ${num} ${errorString(num)}:\n`);
            log.small.raw('error')(removeErrorFooter(stdout));
        } else if (err) {
            log.small.raw('warn', 'red', HEAVY_EXCLAMATION_MARK_SYMBOL)(
                'Flow terminated abnormally - you may have an error in your configuration.\n');
            log.small.raw('error')(err);
        } else {
            log.small.success(stdout);
        }
    });
};
<commit_msg>spacing<commit_after>import { execFile } from 'child_process';
import fs from 'fs';
import path from 'path';

import { initLog } from 'roc';
import flow from 'flow-bin';

import { default as flowconfig } from '../resources/flowconfig';

const log = initLog();
const HEAVY_EXCLAMATION_MARK_SYMBOL = '❗';

const expectedOutput = stdout => /Found \d+ error[s]?/.test(stdout);

const numErrors = (stdout) => /Found (\d+) error[s]?/.exec(stdout)[1];

const removeErrorFooter = (stdout) => stdout.replace(/Found \d+ error[s]?/, '');

const errorString = (num) => ((num > 1) ? 'errors' : 'error');

const configFileExists = () => fs.existsSync(path.join(process.cwd(), '.flowconfig'));

const createConfigFile = () => {
    const target = path.join(process.cwd(), '.flowconfig');

    fs.writeFileSync(target, flowconfig);
};

export default () => () => {
    if (!configFileExists()) {
        log.small.warn('No .flowconfig file found - creating.');
        log.small.info('You should commit .flowconfig to your source code repsository.');
        createConfigFile();
    } else {
        log.small.info('Using .flowconfig file in root of project.');
    }

    log.small.info('Starting Flow type check. This may take a short while, please be patient.\n');

    return () => execFile(flow, ['check'], (err, stdout) => {
        if (err && expectedOutput(stdout)) {
            const num = numErrors(stdout);

            log.small.raw('warn', 'red', HEAVY_EXCLAMATION_MARK_SYMBOL)(`Flow found ${num} ${errorString(num)}:\n`);
            log.small.raw('error')(removeErrorFooter(stdout));
        } else if (err) {
            log.small.raw('warn', 'red', HEAVY_EXCLAMATION_MARK_SYMBOL)(
                'Flow terminated abnormally - you may have an error in your configuration.\n');
            log.small.raw('error')(err);
        } else {
            log.small.success(stdout);
        }
    });
};
<|endoftext|>


<commit_before>var jsdom   = require('jsdom');
var request = require('request');
var _       = require('underscore');
var xpath   = require('jsdom-xpath');
var expect  = require('./expect');
var URL     = require('url');
var utils   = require('./utils');

function getAttribute(attr) {
  return function(node) {
    return _.isUndefined(attr) ? node.innerHTML : node.getAttribute(attr);
  }
}

function attr(task, callback) {
  expect.hasObjectNodes(task);
  _.each(task.cols, function(column) {
    column.nodes = _.map(column.nodes, getAttribute(column.attr));
  });
  callback(task);
}

function scrape(task, callback) {
  utils.get(task.url, function(err, resp, body) {
    // TODO: error handling ;-)
    var window = jsdom.jsdom(body).parentWindow;
    task.cols = _.map(task.cols, function(column) {
      var nodes = xpath(column.sel, window.document);
      return _.extend(column, { nodes: nodes });
    });
    callback(task);
  });
}

function fneval(task, callback) {
  expect.hasStringNodes(task);
  _.each(task.cols, function(col) {
    if(typeof col.fn === "function") {
      col.nodes = _.map(col.nodes, col.fn);
    }
  });
  callback(task);
}

function sendResults(results, callback) {
  return function() {
    callback(results);
  };
}

function zip(subjects, callback) {
  var longest  = _.max(_.pluck(_.pluck(subjects, 'nodes'), 'length'));
  _.times(longest, function(i) {
    var results = {};
    var done = _.after(subjects.length, sendResults(results, callback));
    _.each(subjects, function(v) {
      if (!hasColumns(v)) {
        results[v.desc] = v.nodes[i];
        done();
      } else {
        zip(v.nodes[i].cols, function(res) {
          results[v.desc] = (res);
          done();
        });
      }
    });
  });
}

var hasColumns = function (obj) {
  return obj.cols;
};

function unnest (task, callback) {
  var nested = task.cols.filter(hasColumns);
  if (!nested||!nested.length) {return callback(task);}
  var total = (nested.length * nested[0].nodes.length);
  var done = _.after(total,function(){callback(task);});
  nested.forEach(function(column) {
    column.nodes.forEach(function(url,index) {
      var absoluteURL = URL.resolve(task.url, url);
      fetch({url: absoluteURL, cols: column.cols}, (function(d,i) {
        return function (res) {
          d.nodes[i] = utils.clone(res);
          done();
        };
      })(column, index));
    });
  });
}

function fetch(definition, callback) {
  expect.isNormalised(definition);
  scrape(definition, function(t0) {
    attr(t0, function(t1) {
      fneval(t1, function(t2) {
        unnest(t2, callback);
      });
    });
  });
}

module.exports = function(spec, cb) {
  fetch(spec, function(subj) {
    zip(subj.cols, cb);
  });
};
<commit_msg>Factored scraping, fn. exec code. Renamed the attributes and unnesting functions.<commit_after>var jsdom   = require('jsdom');
var _       = require('underscore');
var xpath   = require('jsdom-xpath');
var expect  = require('./expect');
var URL     = require('url');
var utils   = require('./utils');

function getAttribute(attr) {
  return function(node) {
    return _.isUndefined(attr) ? node.innerHTML : node.getAttribute(attr);
  }
}

function getAttributes(task, callback) {
  expect.hasObjectNodes(task);
  _.each(task.cols, function(column) {
    column.nodes = _.map(column.nodes, getAttribute(column.attr));
  });
  callback(task);
}

function getContent(window) {
  return function(column) {
    return _.extend(column, {
      nodes: xpath(column.sel, window.document)
    });
  }
}

function getContents(task, callback) {
  utils.get(task.url, function(err, resp, body) {
    var window = jsdom.jsdom(body).parentWindow;
    task.cols = _.map(task.cols, getContent(window));
    callback(task);
  });
}

function runFunction(col) {
  if (typeof col.fn === "function")
    col.nodes = _.map(col.nodes, col.fn);
  return col;
}

function runFunctions(task, callback) {
  expect.hasStringNodes(task);
  _.map(task.cols, runFunction);
  callback(task);
}

function sendResults(results, callback) {
  return function() {
    callback(results);
  };
}

function zip(subjects, callback) {
  var longest  = _.max(_.pluck(_.pluck(subjects, 'nodes'), 'length'));
  _.times(longest, function(i) {
    var results = {};
    var done = _.after(subjects.length, sendResults(results, callback));
    _.each(subjects, function(v) {
      if (!hasColumns(v)) {
        results[v.desc] = v.nodes[i];
        done();
      } else {
        zip(v.nodes[i].cols, function(res) {
          results[v.desc] = (res);
          done();
        });
      }
    });
  });
}

var hasColumns = function (obj) {
  return obj.cols;
};

function resolveNested (task, callback) {
  var nested = task.cols.filter(hasColumns);
  if (!nested||!nested.length) {return callback(task);}
  var total = (nested.length * nested[0].nodes.length);
  var done = _.after(total,function(){callback(task);});
  nested.forEach(function(column) {
    column.nodes.forEach(function(url,index) {
      var absoluteURL = URL.resolve(task.url, url);
      fetch({url: absoluteURL, cols: column.cols}, (function(d,i) {
        return function (res) {
          d.nodes[i] = utils.clone(res);
          done();
        };
      })(column, index));
    });
  });
}

function fetch(definition, callback) {
  expect.isNormalised(definition);
  getContents(definition, function(t0) {
    getAttributes(t0, function(t1) {
      runFunctions(t1, function(t2) {
        resolveNested(t2, callback);
      });
    });
  });
}

module.exports = function(spec, cb) {
  fetch(spec, function(subj) {
    zip(subj.cols, cb);
  });
};
<|endoftext|>