define('package/quiqqer/bricks/bin/Controls/backend/LayoutSettings', [

    'qui/controls/Control',
    'qui/controls/windows/Confirm',
    'Ajax',
    'Locale',
    'Projects',
    'package/quiqqer/bricks/bin/Controls/backend/BlockSlot',
    URL_OPT_DIR + 'bin/quiqqer-asset/gridstack/gridstack/dist/gridstack-all.js',

    'css!' + URL_OPT_DIR + 'bin/quiqqer-asset/gridstack/gridstack/dist/gridstack.min.css',
    'css!package/quiqqer/bricks/bin/Controls/backend/LayoutSettings.css'

], function (QUIControl, QUIConfirm, QUIAjax, QUILocale, Projects, BlockSlot, GridStackModule) {
    "use strict";

    const GridStack = (GridStackModule && GridStackModule.GridStack) || window.GridStack || GridStackModule;

    const lg = 'quiqqer/bricks';
    const MODE_EDITOR = 'editor';
    const MODE_BRICK = 'brick';
    const MODE_IMAGE = 'image';
    const MODE_SUB_LAYOUT = 'subLayout';
    const EDIT_MODE_CONTENT = 'content';
    const EDIT_MODE_LAYOUT = 'layout';
    const IMAGE_FIT_AUTO = 'auto';
    const IMAGE_FIT_COVER = 'cover';
    const IMAGE_FIT_CONTAIN = 'contain';
    const BACKGROUND_POSITION_LEFT_TOP = 'left top';
    const BACKGROUND_POSITION_TOP = 'center top';
    const BACKGROUND_POSITION_RIGHT_TOP = 'right top';
    const BACKGROUND_POSITION_LEFT = 'left center';
    const BACKGROUND_POSITION_CENTER = 'center center';
    const BACKGROUND_POSITION_RIGHT = 'right center';
    const BACKGROUND_POSITION_LEFT_BOTTOM = 'left bottom';
    const BACKGROUND_POSITION_BOTTOM = 'center bottom';
    const BACKGROUND_POSITION_RIGHT_BOTTOM = 'right bottom';
    const BACKGROUND_POSITION_VALUES = [
        BACKGROUND_POSITION_LEFT_TOP,
        BACKGROUND_POSITION_TOP,
        BACKGROUND_POSITION_RIGHT_TOP,
        BACKGROUND_POSITION_LEFT,
        BACKGROUND_POSITION_CENTER,
        BACKGROUND_POSITION_RIGHT,
        BACKGROUND_POSITION_LEFT_BOTTOM,
        BACKGROUND_POSITION_BOTTOM,
        BACKGROUND_POSITION_RIGHT_BOTTOM
    ];
    const VERTICAL_ALIGN_TOP = 'top';
    const VERTICAL_ALIGN_CENTER = 'center';
    const VERTICAL_ALIGN_BOTTOM = 'bottom';
    const VERTICAL_ALIGN_STRETCH = 'stretch';
    const LINK_TARGET_SELF = '_self';
    const LINK_TARGET_BLANK = '_blank';
    const LINK_REL_OPTIONS = [
        '',
        'nofollow',
        'noopener',
        'noreferrer',
        'noopener noreferrer',
        'nofollow noopener noreferrer'
    ];
    const LINK_TARGET_OPTIONS = [
        '',
        LINK_TARGET_SELF,
        LINK_TARGET_BLANK
    ];
    const TILE_MIN_HEIGHT_NONE = 'none';
    const TILE_MIN_HEIGHT_COMPACT = 'compact';
    const TILE_MIN_HEIGHT_STANDARD = 'standard';
    const TILE_MIN_HEIGHT_LARGE = 'large';
    const TILE_MIN_HEIGHT_EXTRA_LARGE = 'extraLarge';
    const TILE_MIN_HEIGHT_MANUAL = 'manual';
    const DEFAULT_TILE_MIN_HEIGHT_PRESET = TILE_MIN_HEIGHT_STANDARD;
    const CONTENT_PADDING_PRESETS = ['none', 'small', 'normal', 'large', 'extraLarge'];
    const DEFAULT_CONTENT_PADDING_PRESET = 'normal';
    const TILE_MIN_HEIGHT_PRESETS = {
        none: '0px',
        compact: '120px',
        standard: '200px',
        large: '280px',
        extraLarge: '360px',
        manual: null
    };
    const DEFAULT_COLUMNS = 12;
    const MIN_SLOT_WIDTH = 2;
    const GRID_GAP_PRESET_VALUES = {
        none: '0',
        small: '0.5rem',
        normal: '0.85rem',
        large: '1.25rem',
        extraLarge: '1.75rem'
    };
    const BREAKPOINTS = ['desktop', 'tablet', 'mobile'];
    const AJAX_GET_PRESETS = 'package_quiqqer_bricks_ajax_getMultiLayoutPresets';
    const LAYOUT_EDITOR_VIEW_CONTENT = 'content';
    const LAYOUT_EDITOR_VIEW_LAYOUT = 'layout';
    const LAYOUT_EDITOR_VIEW_PRESETS = 'presets';
    const PRESET_ALIASES = {
        'preset-2-equal': 'core:2-equal',
        'preset-2-left-narrow': 'core:2-left-narrow',
        'preset-2-right-narrow': 'core:2-right-narrow',
        'preset-4-equal': 'core:4-equal',
        'preset-4-offset-a': 'core:4-offset-a',
        'preset-4-offset-b': 'core:4-offset-b',
        'preset-3x2-equal': 'core:3x2-equal',
        'preset-3x2-alternating': 'core:3x2-alternating',
        'preset-3rows-middle-full': 'core:3rows-middle-full',
        'preset-top-full-bottom-2': 'core:top-full-bottom-2',
        'preset-top-2-bottom-full': 'core:top-2-bottom-full',
        'preset-left-tall-right-stack': 'core:left-tall-right-stack',
        'preset-right-tall-left-stack': 'core:right-tall-left-stack',
        'preset-center-tall-side-stacks': 'core:center-tall-side-stacks'
    };

    return new Class({

        Extends: QUIControl,
        Type: 'package/quiqqer/bricks/bin/Controls/LayoutSettings',

        Binds: [
            '$onImport',
            '$onAreaChange',
            '$onSlotRemove',
            '$onSlotSelect',
            '$onSubLayoutEdit'
        ],

        options: {
            allowedModes: [MODE_EDITOR, MODE_BRICK, MODE_IMAGE, MODE_SUB_LAYOUT],
            subLayoutAllowedModes: [MODE_EDITOR, MODE_BRICK, MODE_IMAGE],
            fixedSlotModes: null,
            slotAllowedModes: null,
            allowAddSlots: true,
            allowRemoveSlots: true,
            allowMoveSlots: true,
            allowResizeSlots: true
        },

        initialize: function (options) {
            this.parent(options);

            this.$AreaControls = [];
            this.$Input = null;
            this.$Elm = null;
            this.$Container = null;
            this.$Project = null;
            this.$document = null;
            this.$HelperContainer = null;
            this.$Toolbar = null;
            this.$Canvas = null;
            this.$Grid = null;
            this.$GridContainer = null;
            this.$selectedSlotId = '';
            this.$presets = [];
            this.$layoutWindow = null;
            this.$layoutDraft = null;
            this.$layoutBreakpoint = 'desktop';
            this.$layoutToolbar = null;
            this.$layoutCanvas = null;
            this.$layoutEditorView = LAYOUT_EDITOR_VIEW_LAYOUT;
            this.$subLayoutWindow = null;
            this.$subLayoutSlotId = '';
            this.$subLayoutSelectedSlotId = '';
            this.$subLayoutDraft = null;
            this.$subLayoutBreakpoint = 'desktop';
            this.$subLayoutEditorView = LAYOUT_EDITOR_VIEW_CONTENT;
            this.$gridDocument = null;
            this.$gridBreakpoint = '';

            this.addEvents({
                onImport: this.$onImport
            });
        },

        $onImport: function () {
            this.$Input = this.getElm();
            this.$Input.type = 'hidden';
            this.$Input.addClass('quiqqer-bricks-multiLayout-settings-input');

            this.$Elm = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings'
            }).wraps(this.$Input);

            this.$Toolbar = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-toolbar'
            }).inject(this.$Elm);

            this.$Container = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-container'
            }).inject(this.$Elm);

            this.$HelperContainer = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-helper'
            }).inject(this.$Elm);

            this.$loadPresets().then(function () {
                this.$document = this.$normalizeDocument(this.$parseValue(this.$Input.value));
                this.$selectedSlotId = this.$getBreakpointSlots('desktop').length
                    ? this.$getBreakpointSlots('desktop')[0].id
                    : '';

                this.$render();
                this.$update();
            }.bind(this)).catch(function () {
                this.$presets = [];
            }.bind(this));
        },

        $loadPresets: function () {
            return new Promise(function (resolve, reject) {
                QUIAjax.get(AJAX_GET_PRESETS, function (presets) {
                    this.$presets = typeOf(presets) === 'array'
                        ? presets.sort(function (presetA, presetB) {
                            return (presetA.sort || 0) - (presetB.sort || 0);
                        })
                        : [];

                    resolve(this.$presets);
                }.bind(this), {
                    'package': 'quiqqer/bricks',
                    onError: reject
                });
            }.bind(this));
        },

        $getPresets: function () {
            return this.$presets;
        },

        $getDefaultPresetId: function () {
            const presets = this.$getPresets();

            return presets.length ? presets[0].id : null;
        },

        $getConfiguredAllowedModes: function (slotId, allowSubLayout) {
            const slotAllowedModes = this.getAttribute('slotAllowedModes');

            if (slotId
                && typeOf(slotAllowedModes) === 'object'
                && typeOf(slotAllowedModes[slotId]) === 'array'
            ) {
                return slotAllowedModes[slotId];
            }

            const modes = allowSubLayout === false
                ? this.getAttribute('subLayoutAllowedModes')
                : this.getAttribute('allowedModes');

            return typeOf(modes) === 'array' ? modes : [MODE_EDITOR, MODE_BRICK, MODE_IMAGE];
        },

        $getConfiguredFixedMode: function (slotId) {
            const fixedSlotModes = this.getAttribute('fixedSlotModes');

            if (!slotId || typeOf(fixedSlotModes) !== 'object') {
                return '';
            }

            return fixedSlotModes[slotId] || '';
        },

        setProject: function (Project) {
            if (typeOf(Project) === 'string') {
                Project = Projects.get(Project);
            }

            this.$Project = Project && typeof Project.getName === 'function'
                ? Project
                : null;
            this.$applyProjectToChildControls();
        },

        $onAreaChange: function (Control, area, slotId) {
            this.$document.areas[slotId] = this.$normalizeArea(area, this.$getSlotIndex(slotId), slotId);
            this.$selectedSlotId = slotId;
            this.$update();
        },

        $onSlotRemove: function (Control, slotId) {
            this.$removeSlotFromDocument(this.$layoutDraft, slotId);
            this.$selectedSlotId = this.$getBreakpointSlots('desktop', this.$layoutDraft).length
                ? this.$getBreakpointSlots('desktop', this.$layoutDraft)[0].id
                : '';
            this.$renderLayoutEditor();
        },

        $onSlotSelect: function (Control, slotId) {
            if (this.$selectedSlotId === slotId) {
                return;
            }

            this.$selectedSlotId = slotId;

            this.$AreaControls.forEach(function (AreaControl) {
                if (typeof AreaControl.setSelected !== 'function') {
                    return;
                }

                AreaControl.setSelected(AreaControl.getAttribute('slotId') === slotId);
            });
        },

        $onSubLayoutEdit: function (Control, area, slotId) {
            this.$openSubLayoutEditor(slotId);
        },

        $render: function () {
            if (!this.$Container) {
                return;
            }

            this.$destroyAreaControls();

            this.$Toolbar.empty();
            this.$Container.empty();

            this.$renderToolbar();

            this.$Canvas = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-canvas'
            }).inject(this.$Container);

            this.$renderContentCanvas();
        },

        $renderToolbar: function () {
            const ActionGroup = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-toolbarGroup '
                    + 'quiqqer-bricks-multiLayout-settings-toolbarGroup--actions'
            }).inject(this.$Toolbar);

            new Element('button', {
                type: 'button',
                'class': 'btn quiqqer-bricks-multiLayout-settings-button',
                html: '<span class="fa fa-columns"></span><span>'
                    + QUILocale.get(lg, 'brick.multiLayout.toolbar.editLayout') + '</span>',
                events: {
                    click: this.$openLayoutEditor.bind(this)
                }
            }).inject(ActionGroup);

        },

        $renderContentCanvas: function () {
            const Grid = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-contentGrid',
                styles: {
                    gridTemplateColumns: 'repeat(' + this.$getBreakpointColumns('desktop') + ', minmax(0, 1fr))'
                }
            }).inject(this.$Canvas);

            this.$getOrderedSlots('desktop').forEach(function (slot, index) {
                const AreaControl = new BlockSlot({
                    area: this.$document.areas[slot.id],
                    helperContainer: this.$HelperContainer,
                    slotId: slot.id,
                    index: index,
                    interactionMode: EDIT_MODE_CONTENT,
                    allowedModes: this.$getConfiguredAllowedModes(slot.id, true),
                    fixedMode: this.$getConfiguredFixedMode(slot.id),
                    allowModeSwitch: true,
                    settingsVisibility: {
                        contentPaddingPreset: true,
                        verticalAlign: true,
                        customMinHeight: true,
                        customCssClasses: true,
                        background: true,
                        backgroundColor: true,
                        link: true,
                        textColor: true,
                        image: true
                    },
                    events: {
                        change: this.$onAreaChange,
                        editSubLayout: this.$onSubLayoutEdit
                    }
                });

                const Wrap = new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-slotWrap',
                    styles: this.$getSlotGridStyles(slot)
                }).inject(Grid);

                Wrap.setStyle('minHeight', this.$resolveAreaTileMinHeight(this.$document, slot.id));

                AreaControl.inject(Wrap);

                if (this.$Project) {
                    AreaControl.setProject(this.$Project);
                }

                this.$AreaControls.push(AreaControl);
            }.bind(this)    );
        },

        $openSubLayoutEditor: function (slotId) {
            const index = this.$getSlotIndex(slotId);
            const area = this.$document.areas[slotId]
                ? this.$normalizeArea(this.$document.areas[slotId], index, slotId)
                : this.$normalizeArea({mode: MODE_SUB_LAYOUT}, index, slotId);

            area.mode = MODE_SUB_LAYOUT;
            area.subLayoutDocument = this.$normalizeDocument(area.subLayoutDocument, false);
            this.$subLayoutSlotId = slotId;
            this.$subLayoutDraft = this.$cloneDocument(area.subLayoutDocument);
            this.$subLayoutBreakpoint = this.$subLayoutBreakpoint || 'desktop';
            this.$subLayoutEditorView = LAYOUT_EDITOR_VIEW_CONTENT;
            this.$subLayoutSelectedSlotId = this.$getBreakpointSlots('desktop', this.$subLayoutDraft).length
                ? this.$getBreakpointSlots('desktop', this.$subLayoutDraft)[0].id
                : '';

            new QUIConfirm({
                icon: 'fa fa-columns',
                title: QUILocale.get(lg, 'brick.multiLayout.subLayout.editor.title'),
                maxWidth: 1100,
                maxHeight: 900,
                information: QUILocale.get(lg, 'brick.multiLayout.subLayout.editor.information'),
                text: QUILocale.get(lg, 'brick.multiLayout.subLayout.editor.text'),
                events: {
                    onOpen: function (Win) {
                        this.$subLayoutWindow = Win;
                        this.$renderSubLayoutEditor();
                    }.bind(this),
                    onSubmit: function () {
                        const mainIndex = this.$getSlotIndex(this.$subLayoutSlotId);
                        const mainArea = this.$document.areas[this.$subLayoutSlotId] || {};

                        mainArea.mode = MODE_SUB_LAYOUT;
                        mainArea.subLayoutDocument = this.$normalizeDocument(this.$subLayoutDraft, false);
                        this.$document.areas[this.$subLayoutSlotId] = this.$normalizeArea(
                            mainArea,
                            mainIndex,
                            this.$subLayoutSlotId
                        );
                        this.$destroyGrid();
                        this.$subLayoutWindow = null;
                        this.$subLayoutSlotId = '';
                        this.$subLayoutSelectedSlotId = '';
                        this.$subLayoutDraft = null;
                        this.$subLayoutEditorView = LAYOUT_EDITOR_VIEW_CONTENT;
                        this.$render();
                        this.$update();
                    }.bind(this),
                    onClose: function () {
                        this.$destroyGrid();
                        this.$subLayoutWindow = null;
                        this.$subLayoutSlotId = '';
                        this.$subLayoutSelectedSlotId = '';
                        this.$subLayoutDraft = null;
                        this.$subLayoutEditorView = LAYOUT_EDITOR_VIEW_CONTENT;
                        this.$render();
                    }.bind(this)
                }
            }).open();
        },

        $renderSubLayoutEditor: function () {
            if (!this.$subLayoutWindow || !this.$subLayoutDraft) {
                return;
            }

            const Content = this.$subLayoutWindow.getContent();

            this.$destroyGrid();
            this.$destroyAreaControls();
            Content.set('html', '');
            Content.addClass('quiqqer-bricks-multiLayout-settings-editorWindow');

            const Header = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorHeader'
            }).inject(Content);

            new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorHint',
                text: QUILocale.get(lg, 'brick.multiLayout.subLayout.editor.hint')
            }).inject(Header);

            const Toolbar = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorToolbar'
            }).inject(Header);

            [
                {
                    view: LAYOUT_EDITOR_VIEW_CONTENT,
                    icon: 'fa fa-pencil',
                    label: QUILocale.get(lg, 'brick.multiLayout.subLayout.toolbar.editContent')
                },
                {
                    view: LAYOUT_EDITOR_VIEW_LAYOUT,
                    icon: 'fa fa-columns',
                    label: QUILocale.get(lg, 'brick.multiLayout.subLayout.toolbar.editLayout')
                }
            ].forEach(function (entry) {
                new Element('button', {
                    type: 'button',
                    'class': 'btn quiqqer-bricks-multiLayout-settings-breakpointButton'
                        + (this.$subLayoutEditorView === entry.view ? ' is-active' : ''),
                    html: '<span class="' + entry.icon + '"></span><span>' + entry.label + '</span>',
                    events: {
                        click: function () {
                            this.$subLayoutEditorView = entry.view;
                            this.$renderSubLayoutEditor();
                        }.bind(this)
                    }
                }).inject(Toolbar);
            }.bind(this));

            if (this.$subLayoutEditorView === LAYOUT_EDITOR_VIEW_LAYOUT) {
                const breakpointIcons = {
                    desktop: 'fa-solid fa-desktop',
                    tablet: 'fa-solid fa-tablet-screen-button',
                    mobile: 'fa-solid fa-mobile-screen-button'
                };

                BREAKPOINTS.forEach(function (breakpoint) {
                    new Element('button', {
                        type: 'button',
                        'class': 'btn quiqqer-bricks-multiLayout-settings-breakpointButton'
                            + (this.$subLayoutBreakpoint === breakpoint ? ' is-active' : ''),
                        html: '<span class="' + breakpointIcons[breakpoint] + '"></span><span>'
                            + QUILocale.get(lg, 'brick.multiLayout.breakpoint.' + breakpoint) + '</span>',
                        events: {
                            click: function () {
                                this.$subLayoutBreakpoint = breakpoint;
                                this.$renderSubLayoutEditor();
                            }.bind(this)
                        }
                    }).inject(Toolbar);
                }.bind(this));

                if (this.getAttribute('allowAddSlots') !== false) {
                    new Element('button', {
                        type: 'button',
                        'class': 'btn quiqqer-bricks-multiLayout-settings-button',
                        html: '<span class="fa fa-plus"></span><span>'
                            + QUILocale.get(lg, 'brick.multiLayout.toolbar.addSlot') + '</span>',
                        events: {
                            click: function () {
                                this.$addSlotToDocument(this.$subLayoutDraft, false);
                                const slots = this.$getBreakpointSlots('desktop', this.$subLayoutDraft);
                                this.$subLayoutSelectedSlotId = slots.length ? slots[slots.length - 1].id : '';
                                this.$renderSubLayoutEditor();
                            }.bind(this)
                        }
                    }).inject(Toolbar);
                }
            }

            this.$layoutCanvas = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorCanvas'
            }).inject(Content);

            if (this.$subLayoutEditorView === LAYOUT_EDITOR_VIEW_LAYOUT) {
                this.$renderSubLayoutLayoutView();
                return;
            }

            this.$renderSubLayoutContentView();
        },

        $renderSubLayoutContentView: function () {
            const Grid = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-contentGrid',
                styles: {
                    gridTemplateColumns: 'repeat('
                        + this.$getBreakpointColumns('desktop', this.$subLayoutDraft)
                        + ', minmax(0, 1fr))',
                    gap: this.$getGridGapPreviewValue(
                        this.$document.areas[this.$subLayoutSlotId]
                            ? this.$document.areas[this.$subLayoutSlotId].subLayoutGridGapPreset
                            : null
                    )
                }
            }).inject(this.$layoutCanvas);

            this.$getOrderedSlots('desktop', this.$subLayoutDraft).forEach(function (slot, index) {
                const AreaControl = new BlockSlot({
                    area: this.$subLayoutDraft.areas[slot.id],
                    helperContainer: this.$HelperContainer,
                    slotId: slot.id,
                    index: index,
                    interactionMode: EDIT_MODE_CONTENT,
                    allowedModes: this.$getConfiguredAllowedModes(slot.id, false),
                    fixedMode: this.$getConfiguredFixedMode(slot.id),
                    allowModeSwitch: true,
                    settingsVisibility: {
                        contentPaddingPreset: true,
                        verticalAlign: true,
                        customMinHeight: true,
                        customCssClasses: true,
                        background: true,
                        backgroundColor: true,
                        link: true,
                        textColor: true,
                        image: true
                    },
                    events: {
                        change: function (Control, area, slotId) {
                            this.$subLayoutDraft.areas[slotId] = this.$normalizeArea(
                                area,
                                this.$getSlotIndexFromDocument(slotId, this.$subLayoutDraft),
                                slotId,
                                false
                            );
                        }.bind(this)
                    }
                });

                const slotArea = this.$subLayoutDraft.areas[slot.id];
                const Wrap = new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-slotWrap',
                    styles: this.$getSlotGridStyles(slot)
                }).inject(Grid);

                if (this.$document.areas[this.$subLayoutSlotId]
                    && this.$document.areas[this.$subLayoutSlotId].subLayoutAreaBackgroundEnabled !== false
                    && slotArea
                    && !slotArea.backgroundColorEnabled
                ) {
                    Wrap.addClass('is-surface-subtle-enabled');
                }

                Wrap.setStyle(
                    'minHeight',
                    this.$resolveAreaTileMinHeight(
                        this.$subLayoutDraft,
                        slot.id,
                        this.$document.areas[this.$subLayoutSlotId]
                    )
                );
                AreaControl.inject(Wrap);

                if (this.$Project) {
                    AreaControl.setProject(this.$Project);
                }

                this.$AreaControls.push(AreaControl);
            }.bind(this));
        },

        $renderSubLayoutLayoutView: function () {
            const DeviceFrame = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-deviceFrame'
                    + ' quiqqer-bricks-multiLayout-settings-deviceFrame--' + this.$subLayoutBreakpoint
            }).inject(this.$layoutCanvas);

            const GridWrap = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-layoutGridWrap'
            }).inject(DeviceFrame);

            this.$GridContainer = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-layoutGrid grid-stack'
            }).inject(GridWrap);

            this.$getOrderedSlots(this.$subLayoutBreakpoint, this.$subLayoutDraft).forEach(function (slot, index) {
                const Item = new Element('div', {
                    'class': 'grid-stack-item',
                    'gs-id': slot.id,
                    'gs-x': slot.x,
                    'gs-y': slot.y,
                    'gs-w': slot.w,
                    'gs-h': slot.h,
                    'gs-min-w': MIN_SLOT_WIDTH
                }).inject(this.$GridContainer);

                const ContentElm = new Element('div', {
                    'class': 'grid-stack-item-content'
                }).inject(Item);

                const AreaControl = new BlockSlot({
                    area: this.$subLayoutDraft.areas[slot.id],
                    helperContainer: this.$HelperContainer,
                    slotId: slot.id,
                    index: index,
                    interactionMode: EDIT_MODE_LAYOUT,
                    selected: this.$subLayoutSelectedSlotId === slot.id,
                    allowRemoveSlot: this.getAttribute('allowRemoveSlots') !== false
                        && this.$getBreakpointSlots('desktop', this.$subLayoutDraft).length > 1,
                    allowModeSwitch: false,
                    events: {
                        select: function (Control, slotId) {
                            this.$subLayoutSelectedSlotId = slotId;
                            this.$renderSubLayoutEditor();
                        }.bind(this),
                        removeSlot: function (Control, slotId) {
                            this.$removeSlotFromDocument(this.$subLayoutDraft, slotId);
                            this.$subLayoutSelectedSlotId = this.$getBreakpointSlots('desktop', this.$subLayoutDraft).length
                                ? this.$getBreakpointSlots('desktop', this.$subLayoutDraft)[0].id
                                : '';
                            this.$renderSubLayoutEditor();
                        }.bind(this)
                    }
                });

                AreaControl.inject(ContentElm);
                this.$AreaControls.push(AreaControl);
            }.bind(this));

            this.$initGrid(this.$subLayoutDraft, this.$subLayoutBreakpoint);
        },

        $openLayoutEditor: function () {
            this.$layoutDraft = this.$cloneDocument(this.$document);
            this.$layoutBreakpoint = this.$layoutBreakpoint || 'desktop';
            this.$layoutEditorView = LAYOUT_EDITOR_VIEW_LAYOUT;

            new QUIConfirm({
                icon: 'fa fa-columns',
                title: QUILocale.get(lg, 'brick.multiLayout.editor.title'),
                maxWidth: 1100,
                maxHeight: 900,
                information: QUILocale.get(lg, 'brick.multiLayout.editor.information'),
                text: QUILocale.get(lg, 'brick.multiLayout.editor.text'),
                events: {
                    onOpen: function (Win) {
                        this.$layoutWindow = Win;
                        this.$renderLayoutEditor();
                    }.bind(this),
                    onSubmit: function () {
                        this.$document = this.$normalizeDocument(this.$layoutDraft);
                        this.$selectedSlotId = this.$getBreakpointSlots('desktop').some(function (slot) {
                            return slot.id === this.$selectedSlotId;
                        }.bind(this))
                            ? this.$selectedSlotId
                            : (this.$getBreakpointSlots('desktop')[0] ? this.$getBreakpointSlots('desktop')[0].id : '');

                        this.$destroyGrid();
                        this.$layoutDraft = null;
                        this.$layoutWindow = null;
                        this.$render();
                        this.$update();
                    }.bind(this),
                    onClose: function () {
                        this.$destroyGrid();
                        this.$layoutDraft = null;
                        this.$layoutWindow = null;
                        this.$layoutToolbar = null;
                        this.$layoutCanvas = null;
                        this.$layoutEditorView = LAYOUT_EDITOR_VIEW_LAYOUT;
                        this.$render();
                    }.bind(this)
                }
            }).open();
        },

        $renderLayoutEditor: function () {
            if (!this.$layoutWindow) {
                return;
            }

            const Content = this.$layoutWindow.getContent();

            this.$destroyGrid();
            this.$destroyAreaControls();

            Content.set('html', '');
            Content.addClass('quiqqer-bricks-multiLayout-settings-editorWindow');

            const Header = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorHeader'
            }).inject(Content);

            new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorHint',
                text: QUILocale.get(lg, 'brick.multiLayout.editor.breakpointHint')
            }).inject(Header);

            this.$layoutToolbar = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorToolbar'
            }).inject(Header);

            const PresetGroup = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-toolbarGroup '
                    + 'quiqqer-bricks-multiLayout-settings-editorPresetGroup'
            }).inject(this.$layoutToolbar);

            new Element('span', {
                'class': 'quiqqer-bricks-multiLayout-settings-toolbarLabel',
                text: QUILocale.get(lg, 'brick.multiLayout.toolbar.preset')
            }).inject(PresetGroup);

            this.$renderLayoutPresetActions(PresetGroup);

            if (this.$layoutEditorView === LAYOUT_EDITOR_VIEW_LAYOUT) {
                const breakpointIcons = {
                    desktop: 'fa-solid fa-desktop',
                    tablet: 'fa-solid fa-tablet-screen-button',
                    mobile: 'fa-solid fa-mobile-screen-button'
                };

                BREAKPOINTS.forEach(function (breakpoint) {
                    new Element('button', {
                        type: 'button',
                        'class': 'btn quiqqer-bricks-multiLayout-settings-breakpointButton'
                            + (this.$layoutBreakpoint === breakpoint ? ' is-active' : ''),
                        html: '<span class="' + breakpointIcons[breakpoint] + '"></span><span>'
                            + QUILocale.get(lg, 'brick.multiLayout.breakpoint.' + breakpoint) + '</span>',
                        events: {
                            click: function () {
                                if (this.$layoutBreakpoint === breakpoint) {
                                    return;
                                }

                                this.$layoutBreakpoint = breakpoint;
                                this.$renderLayoutEditor();
                            }.bind(this)
                        }
                    }).inject(this.$layoutToolbar);
                }.bind(this));

                const ActionGroup = new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-editorActions'
                }).inject(this.$layoutToolbar);

                if (this.getAttribute('allowAddSlots') !== false) {
                    new Element('button', {
                        type: 'button',
                        'class': 'btn quiqqer-bricks-multiLayout-settings-button',
                        html: '<span class="fa fa-plus"></span><span>'
                            + QUILocale.get(lg, 'brick.multiLayout.toolbar.addSlot') + '</span>',
                        events: {
                            click: function () {
                                this.$addSlotToDocument(this.$layoutDraft);
                                this.$renderLayoutEditor();
                            }.bind(this)
                        }
                    }).inject(ActionGroup);
                }
            }

            this.$layoutCanvas = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-editorCanvas'
            }).inject(Content);

            if (this.$layoutEditorView === LAYOUT_EDITOR_VIEW_PRESETS) {
                this.$renderLayoutPresetGrid();
                return;
            }

            const DeviceFrame = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-deviceFrame'
                    + ' quiqqer-bricks-multiLayout-settings-deviceFrame--' + this.$layoutBreakpoint
            }).inject(this.$layoutCanvas);

            if (this.$layoutBreakpoint === 'desktop') {
                const BrowserBar = new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-browserBar'
                }).inject(DeviceFrame);

                const Dots = new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-browserDots'
                }).inject(BrowserBar);

                ['r', 'y', 'g'].forEach(function (color) {
                    new Element('div', {
                        'class': 'quiqqer-bricks-multiLayout-settings-browserDot'
                            + ' quiqqer-bricks-multiLayout-settings-browserDot--' + color
                    }).inject(Dots);
                });

                new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-browserUrl'
                }).inject(BrowserBar);
            }

            const GridWrap = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-layoutGridWrap'
            }).inject(DeviceFrame);

            if (this.$layoutBreakpoint === 'tablet') {
                new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-tabletNotch'
                }).inject(DeviceFrame);

                new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-tabletHomebar'
                }).inject(DeviceFrame);
            } else if (this.$layoutBreakpoint === 'mobile') {
                new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-mobileIsland'
                }).inject(DeviceFrame);

                new Element('div', {
                    'class': 'quiqqer-bricks-multiLayout-settings-mobileHomebar'
                }).inject(DeviceFrame);
            }

            this.$GridContainer = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-layoutGrid grid-stack'
            }).inject(GridWrap);

            this.$getOrderedSlots(this.$layoutBreakpoint, this.$layoutDraft).forEach(function (slot, index) {
                const Item = new Element('div', {
                    'class': 'grid-stack-item',
                    'gs-id': slot.id,
                    'gs-x': slot.x,
                    'gs-y': slot.y,
                    'gs-w': slot.w,
                    'gs-h': slot.h,
                    'gs-min-w': MIN_SLOT_WIDTH
                }).inject(this.$GridContainer);

                const ContentElm = new Element('div', {
                    'class': 'grid-stack-item-content'
                }).inject(Item);

                const AreaControl = new BlockSlot({
                    area: this.$layoutDraft.areas[slot.id],
                    helperContainer: this.$HelperContainer,
                    slotId: slot.id,
                    index: index,
                    interactionMode: EDIT_MODE_LAYOUT,
                    allowedModes: this.$getConfiguredAllowedModes(slot.id, true),
                    fixedMode: this.$getConfiguredFixedMode(slot.id),
                    selected: this.$selectedSlotId === slot.id,
                    allowRemoveSlot: this.getAttribute('allowRemoveSlots') !== false
                        && this.$getBreakpointSlots('desktop', this.$layoutDraft).length > 1,
                    allowModeSwitch: false,
                    settingsVisibility: {
                        contentPaddingPreset: false,
                        verticalAlign: false,
                        customMinHeight: false,
                        background: false,
                        backgroundColor: false,
                        link: false,
                        textColor: false,
                        image: false
                    },
                    events: {
                        select: this.$onSlotSelect,
                        removeSlot: this.$onSlotRemove
                    }
                });

                AreaControl.inject(ContentElm);
                this.$AreaControls.push(AreaControl);
            }.bind(this));

            this.$initGrid();
        },

        $renderLayoutPresetActions: function (Parent) {
            if (!Parent) {
                return;
            }

            const Actions = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-presetActions'
            }).inject(Parent);

            new Element('button', {
                type: 'button',
                'class': 'btn quiqqer-bricks-multiLayout-settings-button',
                html: '<span class="fa fa-th-large"></span><span>'
                    + QUILocale.get(lg, 'brick.multiLayout.toolbar.choosePreset') + '</span>',
                events: {
                    click: function () {
                        this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_PRESETS);
                    }.bind(this),
                    keydown: function (event) {
                        if (event.key === 'Escape'
                            && this.$layoutEditorView === LAYOUT_EDITOR_VIEW_PRESETS
                        ) {
                            event.preventDefault();
                            this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                        }
                    }.bind(this)
                }
            }).inject(Actions);

            if (this.$layoutEditorView === LAYOUT_EDITOR_VIEW_PRESETS) {
                new Element('button', {
                    type: 'button',
                    'class': 'btn quiqqer-bricks-multiLayout-settings-button',
                    html: '<span class="fa fa-arrow-left"></span><span>'
                        + QUILocale.get(lg, 'brick.multiLayout.toolbar.backToLayout') + '</span>',
                    events: {
                        click: function () {
                            this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                        }.bind(this),
                        keydown: function (event) {
                            if (event.key === 'Escape') {
                                event.preventDefault();
                                this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                            }
                        }.bind(this)
                    }
                }).inject(Actions);
            }
        },

        $renderPresetPreview: function (Parent, preset, options) {
            options = options || {};

            const slots = this.$normalizeSlots(
                preset && preset.slots ? preset.slots : null,
                preset && preset.columns ? preset.columns : DEFAULT_COLUMNS,
                DEFAULT_COLUMNS
            );
            let rows = 1;

            slots.forEach(function (slot) {
                rows = Math.max(rows, slot.y + slot.h);
            });

            const Preview = new Element('span', {
                'class': 'quiqqer-bricks-multiLayout-settings-presetPreview'
                    + (options.compact ? ' is-compact' : ''),
                styles: {
                    gridTemplateColumns: 'repeat(' + DEFAULT_COLUMNS + ', minmax(0, 1fr))',
                    gridTemplateRows: 'repeat(' + rows + ', minmax(0, 1fr))'
                }
            }).inject(Parent);

            slots.forEach(function (slot) {
                new Element('span', {
                    'class': 'quiqqer-bricks-multiLayout-settings-presetPreviewSlot',
                    styles: this.$getSlotGridStyles(slot)
                }).inject(Preview);
            }.bind(this));
        },

        $renderLayoutPresetGrid: function () {
            if (!this.$layoutCanvas) {
                return;
            }

            const currentPreset = this.$getPreset(
                this.$layoutDraft && this.$layoutDraft.preset
                    ? this.$layoutDraft.preset
                    : this.$getDefaultPresetId()
            );
            const Grid = new Element('div', {
                'class': 'quiqqer-bricks-multiLayout-settings-presetGridWrap',
                events: {
                    keydown: function (event) {
                        if (event.key === 'Escape') {
                            event.preventDefault();
                            this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                        }
                    }.bind(this)
                }
            }).inject(this.$layoutCanvas);

            this.$getPresets().forEach(function (preset) {
                const isActive = currentPreset.id === preset.id;
                const Card = new Element('button', {
                    type: 'button',
                    'class': 'quiqqer-bricks-multiLayout-settings-presetCard'
                        + (isActive ? ' is-active' : ''),
                    'aria-pressed': isActive ? 'true' : 'false',
                    title: QUILocale.get(lg, preset.labelKey),
                    'aria-label': QUILocale.get(lg, preset.labelKey),
                    events: {
                        click: function () {
                            this.$changePreset(preset.id, {
                                target: 'layout',
                                onAfterApply: function () {
                                    this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                                }.bind(this)
                            });
                        }.bind(this),
                        keydown: function (event) {
                            if (event.key === 'Escape') {
                                event.preventDefault();
                                this.$setLayoutEditorView(LAYOUT_EDITOR_VIEW_LAYOUT);
                            }
                        }.bind(this)
                    }
                }).inject(Grid);

                this.$renderPresetPreview(Card, preset);
            }.bind(this));
        },

        $setLayoutEditorView: function (view) {
            view = view === LAYOUT_EDITOR_VIEW_PRESETS
                ? LAYOUT_EDITOR_VIEW_PRESETS
                : LAYOUT_EDITOR_VIEW_LAYOUT;

            if (this.$layoutEditorView === view && this.$layoutWindow) {
                this.$renderLayoutEditor();
                return;
            }

            this.$layoutEditorView = view;

            if (this.$layoutWindow) {
                this.$renderLayoutEditor();
            }
        },

        $initGrid: function (documentData, breakpoint) {
            if (!this.$GridContainer || !GridStack) {
                return;
            }

            documentData = documentData || this.$layoutDraft;
            breakpoint = breakpoint || this.$layoutBreakpoint;
            this.$gridDocument = documentData;
            this.$gridBreakpoint = breakpoint;

            this.$Grid = GridStack.init({
                column: this.$getBreakpointColumns(breakpoint, documentData),
                cellHeight: 160,
                disableOneColumnMode: true,
                disableDrag: this.getAttribute('allowMoveSlots') === false,
                disableResize: this.getAttribute('allowResizeSlots') === false,
                draggable: {
                    handle: '.quiqqer-bricks-blockSlot-card',
                    cancel: '.quiqqer-bricks-blockSlot-cardAction'
                },
                float: true,
                margin: 12,
                resizable: {
                    handles: 'e, se, s, sw, w'
                }
            }, this.$GridContainer);

            this.$Grid.on('change', function () {
                this.$syncSlotsFromGrid();
            }.bind(this));
        },

        $syncSlotsFromGrid: function () {
            if (!this.$GridContainer || !this.$gridDocument || !this.$gridBreakpoint) {
                return;
            }

            const slots = [];

            this.$GridContainer.getElements('.grid-stack-item').forEach(function (Item) {
                if (!Item.gridstackNode) {
                    return;
                }

                slots.push(this.$normalizeSlot({
                    id: Item.gridstackNode.id || Item.get('gs-id') || Item.getAttribute('gs-id'),
                    x: Item.gridstackNode.x,
                    y: Item.gridstackNode.y,
                    w: Item.gridstackNode.w,
                    h: Item.gridstackNode.h
                }, slots.length, DEFAULT_COLUMNS, DEFAULT_COLUMNS));
            }.bind(this));

            this.$gridDocument.breakpoints[this.$gridBreakpoint].slots = slots.sort(this.$compareSlots);
        },

        $changePreset: function (presetId, options) {
            options = options || {};

            const target = options.target === 'layout' ? 'layout' : 'document';
            const onAfterApply = typeof options.onAfterApply === 'function'
                ? options.onAfterApply
                : null;
            const targetDocument = target === 'layout' && this.$layoutDraft
                ? this.$layoutDraft
                : this.$document;

            presetId = this.$normalizeLayoutValue(presetId);

            if (!targetDocument) {
                return;
            }

            if (presetId === targetDocument.preset) {
                if (target === 'layout' && this.$layoutWindow) {
                    if (onAfterApply) {
                        onAfterApply();
                        return;
                    }

                    this.$renderLayoutEditor();
                    return;
                }

                if (onAfterApply) {
                    onAfterApply();
                }

                this.$update();
                return;
            }

            const applyPreset = function () {
                const nextDocument = this.$createDocumentFromPreset(presetId, targetDocument);
                const desktopSlots = this.$getBreakpointSlots('desktop', nextDocument);

                if (target === 'layout' && this.$layoutDraft) {
                    this.$layoutDraft = nextDocument;
                } else {
                    this.$document = nextDocument;
                }

                this.$selectedSlotId = desktopSlots.length
                    ? desktopSlots[0].id
                    : '';

                if (target === 'layout' && this.$layoutWindow) {
                    if (onAfterApply) {
                        onAfterApply();
                        return;
                    }

                    this.$renderLayoutEditor();
                    return;
                }

                if (onAfterApply) {
                    onAfterApply();
                }

                this.$render();
                this.$update();
            }.bind(this);

            if (!this.$hasAreaContent(targetDocument)) {
                applyPreset();
                return;
            }

            new QUIConfirm({
                icon: 'fa fa-columns',
                title: QUILocale.get(lg, 'brick.multiLayout.presetChange.title'),
                information: QUILocale.get(lg, 'brick.multiLayout.presetChange.information'),
                text: QUILocale.get(lg, 'brick.multiLayout.presetChange.text'),
                events: {
                    onSubmit: applyPreset
                }
            }).open();
        },

        $destroyGrid: function () {
            if (this.$Grid && typeof this.$Grid.destroy === 'function') {
                this.$Grid.destroy(false);
            }

            this.$Grid = null;
            this.$GridContainer = null;
            this.$gridDocument = null;
            this.$gridBreakpoint = '';
        },

        $destroyAreaControls: function () {
            this.$AreaControls.forEach(function (Control) {
                if (Control && 'destroy' in Control) {
                    Control.destroy();
                }
            });

            this.$AreaControls = [];
        },

        $applyProjectToChildControls: function () {
            this.$AreaControls.forEach(function (Control) {
                if (Control && 'setProject' in Control) {
                    Control.setProject(this.$Project);
                }
            }.bind(this));
        },

        $update: function () {
            this.$Input.value = JSON.encode(this.$document);
        },

        $parseValue: function (value) {
            if (!value) {
                return null;
            }

            try {
                return JSON.decode(value);
            } catch (e) {
                return null;
            }
        },

        $cloneDocument: function (documentData) {
            return JSON.decode(JSON.encode(documentData || {}));
        },

        $normalizeDocument: function (value, allowSubLayout) {
            allowSubLayout = allowSubLayout !== false;

            const preset = this.$getPreset(this.$normalizeLayoutValue(
                value && value.preset ? value.preset : null
            ));
            const desktopColumns = DEFAULT_COLUMNS;
            const desktopSlots = this.$normalizeSlots(
                value && value.breakpoints && value.breakpoints.desktop
                    ? value.breakpoints.desktop.slots
                    : preset.slots,
                value && value.breakpoints && value.breakpoints.desktop
                    ? value.breakpoints.desktop.columns
                    : desktopColumns,
                desktopColumns
            );
            const tabletSlots = this.$normalizeBreakpointSlots(
                value && value.breakpoints && value.breakpoints.tablet
                    ? value.breakpoints.tablet.slots
                    : null,
                desktopSlots,
                'tablet',
                value && value.breakpoints && value.breakpoints.tablet
                    ? value.breakpoints.tablet.columns
                    : desktopColumns
            );
            const mobileSlots = this.$normalizeBreakpointSlots(
                value && value.breakpoints && value.breakpoints.mobile
                    ? value.breakpoints.mobile.slots
                    : null,
                desktopSlots,
                'mobile',
                value && value.breakpoints && value.breakpoints.mobile
                    ? value.breakpoints.mobile.columns
                    : desktopColumns
            );
            const areasSource = value && typeOf(value.areas) === 'object' ? value.areas : {};
            const areas = {};

            desktopSlots.forEach(function (slot, index) {
                areas[slot.id] = this.$normalizeArea(
                    areasSource[slot.id] && typeOf(areasSource[slot.id]) === 'object'
                        ? areasSource[slot.id]
                        : {},
                    index,
                    slot.id,
                    allowSubLayout
                );
            }.bind(this));

            return {
                preset: preset.id,
                breakpoints: {
                    desktop: {
                        columns: desktopColumns,
                        slots: desktopSlots
                    },
                    tablet: {
                        columns: desktopColumns,
                        slots: tabletSlots
                    },
                    mobile: {
                        columns: desktopColumns,
                        slots: mobileSlots
                    }
                },
                areas: areas
            };
        },

        $createDocumentFromPreset: function (presetId, previousDocument) {
            const preset = this.$getPreset(presetId);
            const desktopSlots = this.$normalizeSlots(preset.slots, preset.columns, DEFAULT_COLUMNS);
            const nextDocument = this.$normalizeDocument({
                preset: preset.id,
                breakpoints: {
                    desktop: {
                        columns: DEFAULT_COLUMNS,
                        slots: desktopSlots
                    }
                },
                areas: {}
            });
            const previousSlots = this.$getOrderedSlots('desktop', previousDocument);

            this.$getOrderedSlots('desktop', nextDocument).forEach(function (slot, index) {
                const sameSlotArea = previousDocument.areas[slot.id];
                const mappedSlot = previousSlots[index];
                const mappedArea = mappedSlot && previousDocument.areas[mappedSlot.id]
                    ? previousDocument.areas[mappedSlot.id]
                    : null;

                nextDocument.areas[slot.id] = this.$normalizeArea(
                    sameSlotArea || mappedArea || {},
                    index,
                    slot.id
                );
            }.bind(this));

            return nextDocument;
        },

        $normalizeLayoutValue: function (layout) {
            if (Object.prototype.hasOwnProperty.call(PRESET_ALIASES, layout)) {
                layout = PRESET_ALIASES[layout];
            }

            const found = this.$getPresets().some(function (preset) {
                return preset.id === layout;
            });

            return found ? layout : this.$getDefaultPresetId();
        },

        $normalizeSlots: function (slots, sourceColumns, targetColumns) {
            const normalized = [];
            const used = {};
            sourceColumns = parseInt(sourceColumns, 10) || DEFAULT_COLUMNS;
            targetColumns = parseInt(targetColumns, 10) || sourceColumns;

            if (typeOf(slots) !== 'array') {
                slots = this.$getPreset(this.$getDefaultPresetId()).slots;
            }

            slots.forEach(function (slot, index) {
                slot = this.$normalizeSlot(slot, index, sourceColumns, targetColumns);

                if (used[slot.id]) {
                    slot.id = this.$createUniqueSlotId(used);
                }

                used[slot.id] = true;
                normalized.push(slot);
            }.bind(this));

            return normalized.length
                ? normalized.sort(this.$compareSlots)
                : this.$getPreset(this.$getDefaultPresetId()).slots.map(function (slot, index) {
                    return this.$normalizeSlot(slot, index, targetColumns, targetColumns);
                }.bind(this));
        },

        $normalizeBreakpointSlots: function (slots, desktopSlots, breakpoint, sourceColumns) {
            const fallback = breakpoint === 'mobile'
                ? this.$buildMobileDefaultSlots(desktopSlots)
                : this.$buildTabletDefaultSlots(desktopSlots);
            const normalizedById = {};

            if (typeOf(slots) === 'array') {
                slots.forEach(function (slot, index) {
                    const normalizedSlot = this.$normalizeSlot(
                        slot,
                        index,
                        sourceColumns,
                        DEFAULT_COLUMNS
                    );

                    normalizedById[normalizedSlot.id] = normalizedSlot;
                }.bind(this));
            }

            const normalized = desktopSlots.map(function (desktopSlot, index) {
                const sourceSlot = normalizedById[desktopSlot.id] || fallback[index] || desktopSlot;
                const slot = this.$normalizeSlot(sourceSlot, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS);

                slot.id = desktopSlot.id;

                return slot;
            }.bind(this)).sort(this.$compareSlots);

            if (!this.$hasOverlappingSlots(normalized)) {
                return normalized;
            }

            return fallback.map(function (slot, index) {
                slot = this.$normalizeSlot(slot || desktopSlots[index], index, DEFAULT_COLUMNS, DEFAULT_COLUMNS);
                slot.id = desktopSlots[index].id;

                return slot;
            }.bind(this)).sort(this.$compareSlots);
        },

        $buildTabletDefaultSlots: function (desktopSlots) {
            return desktopSlots.map(function (slot, index) {
                return this.$normalizeSlot(slot, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS);
            }.bind(this));
        },

        $buildMobileDefaultSlots: function (desktopSlots) {
            let currentY = 0;

            return desktopSlots.slice().sort(this.$compareSlots).map(function (slot) {
                const height = Math.max(1, parseInt(slot.h, 10) || 1);
                const mobileSlot = {
                    id: slot.id,
                    x: 0,
                    y: currentY,
                    w: DEFAULT_COLUMNS,
                    h: height
                };

                currentY += height;

                return mobileSlot;
            });
        },

        $normalizeSlot: function (slot, index, sourceColumns, targetColumns) {
            slot = typeOf(slot) === 'object' ? slot : {};
            sourceColumns = parseInt(sourceColumns, 10) || DEFAULT_COLUMNS;
            targetColumns = parseInt(targetColumns, 10) || sourceColumns;

            let width = parseInt(slot.w, 10);
            let height = parseInt(slot.h, 10);
            let x = parseInt(slot.x, 10);
            let y = parseInt(slot.y, 10);

            if (isNaN(width) || width < 1) {
                width = 1;
            }

            if (isNaN(height) || height < 1) {
                height = 1;
            }

            if (sourceColumns !== targetColumns) {
                const ratio = targetColumns / sourceColumns;

                width = Math.max(1, Math.round(width * ratio));
                x = isNaN(x) ? 0 : Math.round(x * ratio);
            }

            width = Math.min(targetColumns, width);
            x = isNaN(x) || x < 0 ? 0 : Math.min(targetColumns - width, x);
            y = isNaN(y) || y < 0 ? index : y;

            return {
                id: slot.id ? slot.id.toString() : 'slot-' + (index + 1),
                x: x,
                y: y,
                w: width,
                h: height
            };
        },

        $normalizeArea: function (area, index, slotId, allowSubLayout) {
            area = typeOf(area) === 'object' ? area : {};
            allowSubLayout = allowSubLayout !== false;

            let mode = area.mode;
            const allowedModes = allowSubLayout
                ? [MODE_EDITOR, MODE_BRICK, MODE_IMAGE, MODE_SUB_LAYOUT]
                : [MODE_EDITOR, MODE_BRICK, MODE_IMAGE];

            if (allowedModes.indexOf(mode) === -1) {
                mode = MODE_EDITOR;
            }

            let imageFit = area.imageFit;

            if ([IMAGE_FIT_AUTO, IMAGE_FIT_COVER, IMAGE_FIT_CONTAIN].indexOf(imageFit) === -1) {
                imageFit = IMAGE_FIT_AUTO;
            }

            let verticalAlign = area.verticalAlign;

            if ([VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_CENTER, VERTICAL_ALIGN_BOTTOM, VERTICAL_ALIGN_STRETCH].indexOf(verticalAlign) === -1) {
                verticalAlign = VERTICAL_ALIGN_CENTER;
            }

            let backgroundImageFit = area.backgroundImageFit;

            if ([IMAGE_FIT_AUTO, IMAGE_FIT_COVER, IMAGE_FIT_CONTAIN].indexOf(backgroundImageFit) === -1) {
                backgroundImageFit = IMAGE_FIT_COVER;
            }

            let backgroundImagePosition = area.backgroundImagePosition;

            if (BACKGROUND_POSITION_VALUES.indexOf(backgroundImagePosition) === -1) {
                backgroundImagePosition = BACKGROUND_POSITION_CENTER;
            }

            let imagePosition = area.imagePosition;

            if (BACKGROUND_POSITION_VALUES.indexOf(imagePosition) === -1) {
                imagePosition = BACKGROUND_POSITION_CENTER;
            }

            let backgroundOverlayOpacity = parseInt(area.backgroundOverlayOpacity, 10);

            if (isNaN(backgroundOverlayOpacity)) {
                backgroundOverlayOpacity = 100;
            }

            backgroundOverlayOpacity = Math.max(0, Math.min(100, backgroundOverlayOpacity));

            const link = this.$normalizeLink(area.link);

            const normalized = {
                title: area.title || QUILocale.get(lg, 'brick.multiLayout.area.label', {
                    number: index + 1
                }),
                mode: mode,
                contentPaddingPreset: this.$normalizeContentPaddingPreset(area.contentPaddingPreset),
                content: area.content || '',
                brickId: area.brickId ? parseInt(area.brickId, 10) || 0 : 0,
                brickTitle: area.brickTitle || '',
                brickType: area.brickType || '',
                image: area.image || '',
                imageFit: imageFit,
                imageWidth: area.imageWidth ? area.imageWidth.toString().trim() : '',
                imageHeight: area.imageHeight ? area.imageHeight.toString().trim() : '',
                imagePosition: imagePosition,
                backgroundEnabled: !!area.backgroundEnabled,
                backgroundImage: area.backgroundImage || '',
                backgroundImageFit: backgroundImageFit,
                backgroundImagePosition: backgroundImagePosition,
                backgroundOverlayEnabled: !!area.backgroundOverlayEnabled,
                backgroundOverlayColor: area.backgroundOverlayColor || '#000000',
                backgroundOverlayOpacity: backgroundOverlayOpacity,
                backgroundColorEnabled: !!area.backgroundColorEnabled,
                backgroundColor: area.backgroundColor || '#000000',
                textColor: area.textColor ? area.textColor.toString().trim() : '',
                customMinHeightEnabled: !!area.customMinHeightEnabled,
                customMinHeightPreset: this.$normalizeTileMinHeightPreset(area.customMinHeightPreset),
                customMinHeightValue: area.customMinHeightValue
                    ? area.customMinHeightValue.toString().trim()
                    : '',
                customCssClasses: this.$normalizeCustomCssClasses(area.customCssClasses),
                link: link,
                verticalAlign: verticalAlign
            };

            if (mode === MODE_SUB_LAYOUT) {
                normalized.subLayoutDocument = this.$normalizeDocument(area.subLayoutDocument, false);
                normalized.subLayoutAreaBackgroundEnabled =
                    area.subLayoutAreaBackgroundEnabled !== false;
                normalized.subLayoutGridGapPreset = this.$normalizeGridGapPreset(
                    area.subLayoutGridGapPreset
                );
                normalized.subLayoutTileMinHeightPreset = this.$normalizeTileMinHeightPreset(
                    area.subLayoutTileMinHeightPreset
                );
                normalized.subLayoutTileMinHeightValue = area.subLayoutTileMinHeightValue
                    ? area.subLayoutTileMinHeightValue.toString().trim()
                    : '';
            }

            return normalized;
        },

        $normalizeGridGapPreset: function (preset) {
            return ['none', 'small', 'normal', 'large', 'extraLarge'].indexOf(preset) !== -1
                ? preset
                : 'normal';
        },

        $normalizeCustomCssClasses: function (value) {
            if (value === null || value === undefined) {
                return '';
            }

            return value.toString().split(',').map(function (entry) {
                return entry.trim();
            }).filter(function (entry) {
                return entry !== '';
            }).join(', ');
        },

        $normalizeCssSizeValue: function (value) {
            if (value === null || value === undefined) {
                return '';
            }

            value = value.toString().trim();

            if (value === '') {
                return '';
            }

            if (/^-?\d+(?:\.\d+)?$/.test(value)) {
                return value + 'px';
            }

            return value;
        },

        $getGridGapPreviewValue: function (preset) {
            return GRID_GAP_PRESET_VALUES[this.$normalizeGridGapPreset(preset)];
        },

        $normalizeContentPaddingPreset: function (preset) {
            return CONTENT_PADDING_PRESETS.indexOf(preset) !== -1
                ? preset
                : DEFAULT_CONTENT_PADDING_PRESET;
        },

        $normalizeTileMinHeightPreset: function (preset) {
            return Object.prototype.hasOwnProperty.call(TILE_MIN_HEIGHT_PRESETS, preset)
                ? preset
                : DEFAULT_TILE_MIN_HEIGHT_PRESET;
        },

        $resolveTileMinHeight: function (preset, value) {
            preset = this.$normalizeTileMinHeightPreset(preset);

            if (preset === TILE_MIN_HEIGHT_MANUAL) {
                value = this.$normalizeCssSizeValue(value);

                if (value) {
                    return value;
                }

                return TILE_MIN_HEIGHT_PRESETS[DEFAULT_TILE_MIN_HEIGHT_PRESET];
            }

            return TILE_MIN_HEIGHT_PRESETS[preset];
        },

        $resolveAreaTileMinHeight: function (documentData, slotId, rootArea) {
            documentData = documentData || this.$document;

            const area = documentData.areas && documentData.areas[slotId]
                ? documentData.areas[slotId]
                : null;

            if (area && area.customMinHeightEnabled) {
                const customValue = this.$resolveCustomTileMinHeight(area);

                if (customValue) {
                    return customValue;
                }
            }

            if (rootArea && rootArea.subLayoutTileMinHeightPreset) {
                return this.$resolveTileMinHeight(
                    rootArea.subLayoutTileMinHeightPreset,
                    rootArea.subLayoutTileMinHeightValue
                );
            }

            return this.$resolveTileMinHeight(
                this.$getGlobalTileMinHeightPreset(),
                this.$getGlobalTileMinHeightValue()
            );
        },

        $getGlobalTileMinHeightPreset: function () {
            const Field = document.body.getElement('[name="tileMinHeightPreset"]');

            return this.$normalizeTileMinHeightPreset(Field ? Field.value : null);
        },

        $getGlobalTileMinHeightValue: function () {
            const Field = document.body.getElement('[name="tileMinHeightValue"]');

            return Field ? Field.value.toString().trim() : '';
        },

        $resolveCustomTileMinHeight: function (area) {
            const preset = this.$normalizeTileMinHeightPreset(area.customMinHeightPreset);

            if (preset === TILE_MIN_HEIGHT_MANUAL) {
                return this.$normalizeCssSizeValue(
                    area.customMinHeightValue
                        ? area.customMinHeightValue.toString().trim()
                        : ''
                );
            }

            return TILE_MIN_HEIGHT_PRESETS[preset];
        },

        $normalizeLink: function (link) {
            if (typeOf(link) !== 'object') {
                return null;
            }

            const href = link.href ? link.href.toString().trim() : '';

            if (!href) {
                return null;
            }

            return {
                href: href,
                rel: LINK_REL_OPTIONS.indexOf(link.rel) !== -1 ? link.rel : '',
                target: LINK_TARGET_OPTIONS.indexOf(link.target) !== -1 ? link.target : '',
                title: link.title ? link.title.toString().trim() : ''
            };
        },

        $getSlotGridStyles: function (slot) {
            return {
                gridColumn: (slot.x + 1) + ' / span ' + slot.w,
                gridRow: (slot.y + 1) + ' / span ' + slot.h
            };
        },

        $hasAreaContent: function (documentData) {
            documentData = documentData || this.$document;

            return this.$getBreakpointSlots('desktop', documentData).some(function (slot) {
                const area = documentData.areas[slot.id];

                if (!area) {
                    return false;
                }

                return !!(
                    (area.content && area.content.trim())
                    || area.brickId
                    || area.image
                    || area.mode === MODE_SUB_LAYOUT
                );
            }.bind(this));
        },

        $getPreset: function (presetId) {
            let found = this.$getPresets().filter(function (preset) {
                return preset.id === presetId;
            })[0];

            if (!found) {
                found = this.$getPresets()[0];
            }

            return {
                id: found.id,
                labelKey: found.labelKey,
                columns: DEFAULT_COLUMNS,
                defaultSlotWidth: found.defaultSlotWidth,
                slots: found.slots.map(function (slot, index) {
                    return this.$normalizeSlot(slot, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS);
                }.bind(this))
            };
        },

        $getDefaultSlotWidth: function (documentData) {
            const preset = this.$getPreset(
                documentData && documentData.preset
                    ? documentData.preset
                    : (this.$document && this.$document.preset)
            );
            let width = parseInt(preset.defaultSlotWidth, 10);

            if (isNaN(width) || width < 1) {
                width = DEFAULT_COLUMNS;
            }

            return Math.max(1, Math.min(DEFAULT_COLUMNS, width));
        },

        $getBreakpointColumns: function (breakpoint, documentData) {
            documentData = documentData || this.$document;

            if (!documentData
                || !documentData.breakpoints
                || !documentData.breakpoints[breakpoint]
            ) {
                return DEFAULT_COLUMNS;
            }

            return parseInt(documentData.breakpoints[breakpoint].columns, 10) || DEFAULT_COLUMNS;
        },

        $getBreakpointSlots: function (breakpoint, documentData) {
            documentData = documentData || this.$document;

            if (!documentData
                || !documentData.breakpoints
                || !documentData.breakpoints[breakpoint]
                || typeOf(documentData.breakpoints[breakpoint].slots) !== 'array'
            ) {
                return [];
            }

            return documentData.breakpoints[breakpoint].slots;
        },

        $getOrderedSlots: function (breakpoint, documentData) {
            return this.$getBreakpointSlots(breakpoint, documentData).slice().sort(this.$compareSlots);
        },

        $compareSlots: function (slotA, slotB) {
            if (slotA.y === slotB.y) {
                if (slotA.x === slotB.x) {
                    return slotA.id.localeCompare(slotB.id);
                }

                return slotA.x - slotB.x;
            }

            return slotA.y - slotB.y;
        },

        $hasOverlappingSlots: function (slots) {
            for (let i = 0; i < slots.length; i++) {
                const slotA = slots[i];
                const ax1 = slotA.x;
                const ay1 = slotA.y;
                const ax2 = ax1 + slotA.w;
                const ay2 = ay1 + slotA.h;

                for (let j = i + 1; j < slots.length; j++) {
                    const slotB = slots[j];
                    const bx1 = slotB.x;
                    const by1 = slotB.y;
                    const bx2 = bx1 + slotB.w;
                    const by2 = by1 + slotB.h;

                    if (!(ay1 >= by2 || ay2 <= by1 || ax2 <= bx1 || ax1 >= bx2)) {
                        return true;
                    }
                }
            }

            return false;
        },

        $createUniqueSlotId: function (used) {
            let counter = 1;
            let slotId = 'slot-' + counter;

            while (used[slotId]) {
                counter++;
                slotId = 'slot-' + counter;
            }

            return slotId;
        },

        $createNextSlotId: function (documentData) {
            const used = {};

            this.$getBreakpointSlots('desktop', documentData).forEach(function (slot) {
                used[slot.id] = true;
            });

            return this.$createUniqueSlotId(used);
        },

        $findNextSlotPosition: function (width, height, slots) {
            const columns = DEFAULT_COLUMNS;
            let pointer = 0;

            width = Math.max(1, Math.min(columns, parseInt(width, 10) || 1));
            height = Math.max(1, parseInt(height, 10) || 1);

            while (true) {
                const x = pointer % columns;
                const y = Math.floor(pointer / columns);

                if (x + width > columns) {
                    pointer = (y + 1) * columns;
                    continue;
                }

                if (this.$isSlotAreaFree(x, y, width, height, slots)) {
                    return {
                        x: x,
                        y: y
                    };
                }

                pointer++;
            }
        },

        $getNextStackedY: function (slots) {
            let maxY = 0;

            slots.forEach(function (slot) {
                maxY = Math.max(maxY, slot.y + slot.h);
            });

            return maxY;
        },

        $isSlotAreaFree: function (x, y, width, height, slots) {
            return !slots.some(function (slot) {
                return !(
                    y >= slot.y + slot.h
                    || y + height <= slot.y
                    || x + width <= slot.x
                    || x >= slot.x + slot.w
                );
            });
        },

        $addSlotToDocument: function (documentData, allowSubLayout) {
            allowSubLayout = allowSubLayout !== false;
            const nextId = this.$createNextSlotId(documentData);
            const index = this.$getBreakpointSlots('desktop', documentData).length;
            const defaultSlotWidth = this.$getDefaultSlotWidth(documentData);
            const desktopPosition = this.$findNextSlotPosition(
                defaultSlotWidth,
                1,
                this.$getBreakpointSlots('desktop', documentData)
            );

            documentData.breakpoints.desktop.slots.push(this.$normalizeSlot({
                id: nextId,
                x: desktopPosition.x,
                y: desktopPosition.y,
                w: defaultSlotWidth,
                h: 1
            }, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS));

            documentData.breakpoints.tablet.slots.push(this.$normalizeSlot({
                id: nextId,
                x: desktopPosition.x,
                y: desktopPosition.y,
                w: defaultSlotWidth,
                h: 1
            }, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS));

            documentData.breakpoints.mobile.slots.push(this.$normalizeSlot({
                id: nextId,
                x: 0,
                y: this.$getNextStackedY(this.$getBreakpointSlots('mobile', documentData)),
                w: DEFAULT_COLUMNS,
                h: 1
            }, index, DEFAULT_COLUMNS, DEFAULT_COLUMNS));

            documentData.breakpoints.desktop.slots.sort(this.$compareSlots);
            documentData.breakpoints.tablet.slots.sort(this.$compareSlots);
            documentData.breakpoints.mobile.slots.sort(this.$compareSlots);
            documentData.areas[nextId] = this.$normalizeArea({}, index, nextId, allowSubLayout);
            this.$selectedSlotId = nextId;
        },

        $removeSlotFromDocument: function (documentData, slotId) {
            const desktopSlots = this.$getBreakpointSlots('desktop', documentData).filter(function (slot) {
                return slot.id !== slotId;
            });

            if (!desktopSlots.length) {
                return;
            }

            BREAKPOINTS.forEach(function (breakpoint) {
                documentData.breakpoints[breakpoint].slots = this.$getBreakpointSlots(
                    breakpoint,
                    documentData
                ).filter(function (slot) {
                    return slot.id !== slotId;
                });
            }.bind(this));

            delete documentData.areas[slotId];
        },

        $getSlotIndex: function (slotId) {
            return this.$getSlotIndexFromDocument(slotId, this.$document);
        },

        $getSlotIndexFromDocument: function (slotId, documentData) {
            const slots = this.$getOrderedSlots('desktop', documentData);

            for (let i = 0, len = slots.length; i < len; i++) {
                if (slots[i].id === slotId) {
                    return i;
                }
            }

            return -1;
        }
    });
});
