<commit_before>/**
 * @author Petru Cojocar <petru.cojocar@gmail.com>.
 */
try { angular.module('Web.Components') } catch(err) { angular.module('Web.Components', []) }

angular.module('Web.Components')
    .factory('Web.Components.DataContainerService', ['$sce',
        function ($sce)
        {
            var DataContainerService = {

                /**
                 * NON Singleton Object Instance
                 * @param $model
                 * @returns {*}
                 * @constructor
                 */
                Instance : function ($model)
                {
                    /**
                     * Public instance
                     * @type {{}}
                     */
                    var $this = {
                    };

                    /**
                     * Private instance
                     * @type {{}}
                     */
                    var $private = {
                        model: {}
                    };

                    /********************
                        Public methods
                     ********************/

                    /**
                     * Initializer method
                     * @param $model
                     * @returns {{}}
                     */
                    $this.init = function ($model)
                    {
                        _.each ($model, function (defaultValue, property) {
                            Object.defineProperty($this, property, {
                                get: function () {
                                    return $private.model[property];
                                },
                                set: function (value) {
                                    $private.model[property] = value;
                                },
                                enumerable: true
                            });
                            $private.model[property] = defaultValue;
                        });

                        return $this;
                    };

                    /**
                     * Populate data container
                     * @param data
                     * @param mappings
                     */
                    $this.populateFromData = function (data, mappings)
                    {
                        var withMappings = !_.isUndefined(mappings) && _.isObject(mappings) && _.size(mappings) > 0;

                        _.each (data, function (value, property) {
                            if (!_.isUndefined($private.model[property])) {
                                if (!_.isEqual($private.model[property], value)) {
                                    $private.model[property] = angular.copy(value);
                                }
                            } else {
                                // handle mappings (i.e. id => entity_id or vice-versa)
                                var mappedProperty = withMappings && !_.isUndefined($private.model[mappings[property]]) ? mappings[property] : false;
                                if (mappedProperty && !_.isEqual($private.model[mappedProperty], value)) {
                                    $private.model[mappedProperty] = angular.copy(value);
                                }
                            }
                        });
                    };

                    /**
                     * Get container data
                     * @returns {*}
                     */
                    $this.getData = function ()
                    {
                        return angular.copy($private.model);
                    };

                    /********************
                        Private methods
                     ********************/

                    return $this.init($model);
                }
            };

            return DataContainerService;
        }
    ]);
<commit_msg>added ability to add properties programatically<commit_after>/**
 * @author Petru Cojocar <petru.cojocar@gmail.com>.
 */
try { angular.module('Web.Components') } catch(err) { angular.module('Web.Components', []) }

angular.module('Web.Components')
    .factory('Web.Components.DataContainerService', [
        function ()
        {
            var DataContainerService = {

                /**
                 * NON Singleton Object Instance
                 * @param $model
                 * @returns {*}
                 * @constructor
                 */
                Instance : function ($model)
                {
                    /**
                     * Public instance
                     * @type {{}}
                     */
                    var $this = {
                    };

                    /**
                     * Private instance
                     * @type {{}}
                     */
                    var $private = {
                        model: {}
                    };

                    /********************
                        Public methods
                     ********************/

                    /**
                     * Initializer method
                     * @param $model
                     * @returns {{}}
                     */
                    $this.init = function ($model)
                    {
                        _.each ($model, function (defaultValue, property) {
                            $this.addProperty(property, defaultValue);
                        });

                        return $this;
                    };

                    /**
                     * Add property to model
                     * @param property
                     * @param defaultValue
                     */
                    $this.addProperty = function (property, defaultValue)
                    {
                        defaultValue = defaultValue || null;

                        Object.defineProperty($this, property, {
                            get: function () {
                                return $private.model[property];
                            },
                            set: function (value) {
                                $private.model[property] = value;
                            },
                            enumerable: true
                        });

                        $private.model[property] = defaultValue;
                    };

                    /**
                     * Populate data container
                     * @param data
                     * @param mappings
                     */
                    $this.populateFromData = function (data, mappings)
                    {
                        var withMappings = !_.isUndefined(mappings) && _.isObject(mappings) && _.size(mappings) > 0;

                        _.each (data, function (value, property) {
                            if (!_.isUndefined($private.model[property])) {
                                if (!_.isEqual($private.model[property], value)) {
                                    $private.model[property] = angular.copy(value);
                                }
                            } else {
                                // handle mappings (i.e. id => entity_id or vice-versa)
                                var mappedProperty = withMappings && !_.isUndefined($private.model[mappings[property]]) ? mappings[property] : false;
                                if (mappedProperty && !_.isEqual($private.model[mappedProperty], value)) {
                                    $private.model[mappedProperty] = angular.copy(value);
                                }
                            }
                        });
                    };

                    /**
                     * Get container data
                     * @returns {*}
                     */
                    $this.getData = function ()
                    {
                        return angular.copy($private.model);
                    };

                    /********************
                        Private methods
                     ********************/

                    return $this.init($model);
                }
            };

            return DataContainerService;
        }
    ]);
<|endoftext|>


<commit_before>define(function (require) {
    'use strict';
    var OverlayView,
        BaseView = require('oroui/js/app/views/base/view'),
        JsplumbAreaView = require('./jsplumb/area');

    OverlayView = BaseView.extend({
        listen: {
            'change model': 'render'
        },

        initialize: function (options) {
            if (!(options.areaView instanceof JsplumbAreaView)) {
                throw new Error('areaView options is required and must be a JsplumbAreaView');
            }
            this.areaView = options.areaView;
            OverlayView.__super__.initialize.apply(this, arguments);
        },

        ensureAttributes: function () {
            // css class is updated by jsPlumb, use attribute instead
            this.$el.attr('data-role', 'transition-overlay');
        },

        render: function () {
            OverlayView.__super__.render.apply(this, arguments);
            this.ensureAttributes();
        }
    });

    return OverlayView;
});
<commit_msg>BAP-8172 comments added<commit_after>define(function (require) {
    'use strict';
    var OverlayView,
        BaseView = require('oroui/js/app/views/base/view'),
        JsplumbAreaView = require('./area');

    OverlayView = BaseView.extend({
        listen: {
            'change model': 'render'
        },

        initialize: function (options) {
            if (!(options.areaView instanceof JsplumbAreaView)) {
                throw new Error('areaView options is required and must be a JsplumbAreaView');
            }
            this.areaView = options.areaView;
            OverlayView.__super__.initialize.apply(this, arguments);
        },

        ensureAttributes: function () {
            // css class is updated by jsPlumb, use attribute instead
            this.$el.attr('data-role', 'transition-overlay');
        },

        render: function () {
            OverlayView.__super__.render.apply(this, arguments);
            this.ensureAttributes();
        }
    });

    return OverlayView;
});
<|endoftext|>


<commit_before>import React from 'react';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
import { Tab, Tabs } from 'material-ui';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import SwipeableViews from 'react-swipeable-views';
import DashboardSection from '../../components/DashboardPage/DashboardSection';

describe('DashboardSection component', () => {
    const muiTheme = getMuiTheme();
    let props;
    let wrapper;
    let instance;

    beforeEach(() => {
        setup();
    });

    function setup({ propsOverride = {}, mounted = false, children = [] } = {}) {
        props = {
            title: 'Test',
            name: 'test',
            columns: 3,
            providers: [],
            noDataElement:
,
            ...propsOverride,
        };

        if (mounted) {
            wrapper = mount(
                <DashboardSection { ...props }>
                    {children}
                </DashboardSection>,
                {
                    context: { muiTheme },
                    childContextTypes: { muiTheme: React.PropTypes.object },
                }
            );
        } else {
            wrapper = shallow(
                <DashboardSection { ...props }>
                    {children}
                </DashboardSection>
            );
        }

        instance = wrapper.instance();
    }

    function generateChildren(count) {
        const children = [];
        for (let i = 0; i < count; i++) {
            children.push(
);
        }

        return children;
    }

    it('renders header', () => {
        expect(wrapper.find('.qa-DashboardSection-Header')).toHaveLength(1);
    });

    it('renders header title', () => {
        expect(wrapper.find('.qa-DashboardSection-Header-Title').text()).toBe(props.title);
    });

    it('limits the number of pages to maxPages', () => {
        setup({
            propsOverride: {
                columns: 1,
                rows: 1,
            },
            children: generateChildren(10),
        });
        expect(wrapper.find('.qa-DashboardSection-Page-Item')).toHaveLength(instance.itemsPerPage * instance.maxPages);
    });

    it('constructs the correct number of pages, each with the correct number of children', () => {
        setup({
            propsOverride: {
                columns: 3,
                rows: 2,
            },
            children: generateChildren(8),
        });
        expect(instance.maxPages).toBe(3);
        const pages = instance.getPages();
        expect(pages).toHaveLength(2);
        expect(pages[0]).toHaveLength(6);
        expect(pages[1]).toHaveLength(2);
    });

    it('updates the page index on a page change', () => {
        expect(wrapper.state().pageIndex).not.toBe(1);
        instance.handlePageChange(1);
        expect(wrapper.state().pageIndex).toBe(1);
    });

    describe('no children', () => {
        it('renders "no data" element', () => {
            expect(wrapper.find('.qa-DashboardSection-NoDataElement')).toHaveLength(1);
        });
    });

    describe('3 columns, 3 children (1 page)', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    columns: 3,
                },
                children: generateChildren(3),
            });
        });

        it('renders a single page', () => {
            expect(wrapper.find('.qa-DashboardSection-Page')).toHaveLength(1);
        });

        it('constructs a single page of items', () => {
            expect(instance.getPages().length).toBe(1);
        });

        it('renders a tab group', () => {
            expect(wrapper.find(Tabs)).toHaveLength(1);
        });

        it('renders 3 tab buttons (maxPages = 3)', () => {
            expect(wrapper.find(Tab)).toHaveLength(3);
        });

        it('enables the first tab button', () => {
            const tabButton = wrapper.find(Tab).at(0);
            expect(tabButton.props().style.pointerEvents).not.toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).toBe('white');
        });

        it('disables the second tab button', () => {
            const tabButton = wrapper.find(Tab).at(1);
            expect(tabButton.props().style.pointerEvents).toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).not.toBe('white');
        });

        it('disables the third tab button', () => {
            const tabButton = wrapper.find(Tab).at(2);
            expect(wrapper.find(Tab).at(2).props().style.pointerEvents).toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).not.toBe('white');
        });

        it('renders SwipeableViews component', () => {
            expect(wrapper.find(SwipeableViews)).toHaveLength(1);
        });

        it('syncs page index with SwipeableViews component', () => {
            const swipeableViews = wrapper.find(SwipeableViews);
            expect(swipeableViews.props().index).toBe(instance.state.pageIndex);
            expect(swipeableViews.props().onChangeIndex).toBe(instance.handlePageChange);
        });

        it('renders children', () => {
            expect(wrapper.find('.qa-DashboardSection-Child')).toHaveLength(3);
        });
    });

    describe('when rowMajor is true', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    rowMajor: true,
                    columns: 3,
                    rows: 3,
                },
                children: generateChildren(9)
            });
        });

        it('renders children in a row-major grid', () => {
            expect(wrapper.find('.qa-DashboardSection-Page-Column')).toHaveLength(0);
        });
    });

    describe('when rowMajor is false', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    rowMajor: false,
                    columns: 3,
                    rows: 3,
                },
                children: generateChildren(9)
            });
        });

        it('renders children in a column-major grid', () => {
            expect(wrapper.find('.qa-DashboardSection-Page-Column')).toHaveLength(3);
        });
    });

    describe('when onViewAll handler is not passed in', () => {
        it('does not render "View All" button', () => {
            expect(wrapper.find('.qa-DashboardSection-ViewAll')).toHaveLength(0);
        });
    });

    describe('when onViewAll handler is passed in', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    onViewAll: sinon.spy(),
                },
                children: generateChildren(1),
            });
        });

        it('renders "View All" button', () => {
            expect(wrapper.find('.qa-DashboardSection-ViewAll')).toHaveLength(1);
        });

        it('calls onViewAll() when "View All" button is clicked', () => {
            wrapper.find('.qa-DashboardSection-ViewAll').simulate('click');
            expect(instance.props.onViewAll.callCount).toBe(1);
        });
    });
});
<commit_msg>Add missing commas in DashboardSection tests.<commit_after>import React from 'react';
import { mount, shallow } from 'enzyme';
import sinon from 'sinon';
import { Tab, Tabs } from 'material-ui';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import SwipeableViews from 'react-swipeable-views';
import DashboardSection from '../../components/DashboardPage/DashboardSection';

describe('DashboardSection component', () => {
    const muiTheme = getMuiTheme();
    let props;
    let wrapper;
    let instance;

    beforeEach(() => {
        setup();
    });

    function setup({ propsOverride = {}, mounted = false, children = [] } = {}) {
        props = {
            title: 'Test',
            name: 'test',
            columns: 3,
            providers: [],
            noDataElement:
,
            ...propsOverride,
        };

        if (mounted) {
            wrapper = mount(
                <DashboardSection { ...props }>
                    {children}
                </DashboardSection>,
                {
                    context: { muiTheme },
                    childContextTypes: { muiTheme: React.PropTypes.object },
                }
            );
        } else {
            wrapper = shallow(
                <DashboardSection { ...props }>
                    {children}
                </DashboardSection>
            );
        }

        instance = wrapper.instance();
    }

    function generateChildren(count) {
        const children = [];
        for (let i = 0; i < count; i++) {
            children.push(
);
        }

        return children;
    }

    it('renders header', () => {
        expect(wrapper.find('.qa-DashboardSection-Header')).toHaveLength(1);
    });

    it('renders header title', () => {
        expect(wrapper.find('.qa-DashboardSection-Header-Title').text()).toBe(props.title);
    });

    it('limits the number of pages to maxPages', () => {
        setup({
            propsOverride: {
                columns: 1,
                rows: 1,
            },
            children: generateChildren(10),
        });
        expect(wrapper.find('.qa-DashboardSection-Page-Item')).toHaveLength(instance.itemsPerPage * instance.maxPages);
    });

    it('constructs the correct number of pages, each with the correct number of children', () => {
        setup({
            propsOverride: {
                columns: 3,
                rows: 2,
            },
            children: generateChildren(8),
        });
        expect(instance.maxPages).toBe(3);
        const pages = instance.getPages();
        expect(pages).toHaveLength(2);
        expect(pages[0]).toHaveLength(6);
        expect(pages[1]).toHaveLength(2);
    });

    it('updates the page index on a page change', () => {
        expect(wrapper.state().pageIndex).not.toBe(1);
        instance.handlePageChange(1);
        expect(wrapper.state().pageIndex).toBe(1);
    });

    describe('no children', () => {
        it('renders "no data" element', () => {
            expect(wrapper.find('.qa-DashboardSection-NoDataElement')).toHaveLength(1);
        });
    });

    describe('3 columns, 3 children (1 page)', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    columns: 3,
                },
                children: generateChildren(3),
            });
        });

        it('renders a single page', () => {
            expect(wrapper.find('.qa-DashboardSection-Page')).toHaveLength(1);
        });

        it('constructs a single page of items', () => {
            expect(instance.getPages().length).toBe(1);
        });

        it('renders a tab group', () => {
            expect(wrapper.find(Tabs)).toHaveLength(1);
        });

        it('renders 3 tab buttons (maxPages = 3)', () => {
            expect(wrapper.find(Tab)).toHaveLength(3);
        });

        it('enables the first tab button', () => {
            const tabButton = wrapper.find(Tab).at(0);
            expect(tabButton.props().style.pointerEvents).not.toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).toBe('white');
        });

        it('disables the second tab button', () => {
            const tabButton = wrapper.find(Tab).at(1);
            expect(tabButton.props().style.pointerEvents).toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).not.toBe('white');
        });

        it('disables the third tab button', () => {
            const tabButton = wrapper.find(Tab).at(2);
            expect(wrapper.find(Tab).at(2).props().style.pointerEvents).toBe('none');
            expect(tabButton.props().buttonStyle.backgroundColor).not.toBe('white');
        });

        it('renders SwipeableViews component', () => {
            expect(wrapper.find(SwipeableViews)).toHaveLength(1);
        });

        it('syncs page index with SwipeableViews component', () => {
            const swipeableViews = wrapper.find(SwipeableViews);
            expect(swipeableViews.props().index).toBe(instance.state.pageIndex);
            expect(swipeableViews.props().onChangeIndex).toBe(instance.handlePageChange);
        });

        it('renders children', () => {
            expect(wrapper.find('.qa-DashboardSection-Child')).toHaveLength(3);
        });
    });

    describe('when rowMajor is true', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    rowMajor: true,
                    columns: 3,
                    rows: 3,
                },
                children: generateChildren(9),
            });
        });

        it('renders children in a row-major grid', () => {
            expect(wrapper.find('.qa-DashboardSection-Page-Column')).toHaveLength(0);
        });
    });

    describe('when rowMajor is false', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    rowMajor: false,
                    columns: 3,
                    rows: 3,
                },
                children: generateChildren(9),
            });
        });

        it('renders children in a column-major grid', () => {
            expect(wrapper.find('.qa-DashboardSection-Page-Column')).toHaveLength(3);
        });
    });

    describe('when onViewAll handler is not passed in', () => {
        it('does not render "View All" button', () => {
            expect(wrapper.find('.qa-DashboardSection-ViewAll')).toHaveLength(0);
        });
    });

    describe('when onViewAll handler is passed in', () => {
        beforeEach(() => {
            setup({
                propsOverride: {
                    onViewAll: sinon.spy(),
                },
                children: generateChildren(1),
            });
        });

        it('renders "View All" button', () => {
            expect(wrapper.find('.qa-DashboardSection-ViewAll')).toHaveLength(1);
        });

        it('calls onViewAll() when "View All" button is clicked', () => {
            wrapper.find('.qa-DashboardSection-ViewAll').simulate('click');
            expect(instance.props.onViewAll.callCount).toBe(1);
        });
    });
});
<|endoftext|>


<commit_before>/*
 * jQuery File Upload AngularJS Plugin 2.2.0
 * https://github.com/blueimp/jQuery-File-Upload
 *
 * Copyright 2013, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * http://www.opensource.org/licenses/MIT
 */

/* jshint nomen:false */
/* global define, angular */

(function (factory) {
    'use strict';
    if (typeof define === 'function' && define.amd) {
        // Register as an anonymous AMD module:
        define([
            'jquery',
            'angular',
            './jquery.fileupload-image',
            './jquery.fileupload-audio',
            './jquery.fileupload-video',
            './jquery.fileupload-validate'
        ], factory);
    } else {
        factory();
    }
}(function () {
    'use strict';

    angular.module('blueimp.fileupload', [])

        // The fileUpload service provides configuration options
        // for the fileUpload directive and default handlers for
        // File Upload events:
        .provider('fileUpload', function () {
            var scopeEvalAsync = function (expression) {
                    var scope = angular.element(this)
                            .fileupload('option', 'scope');
                    // Schedule a new $digest cycle if not already inside of one
                    // and evaluate the given expression:
                    scope.$evalAsync(expression);
                },
                addFileMethods = function (scope, data) {
                    var files = data.files,
                        file = files[0];
                    angular.forEach(files, function (file, index) {
                        file._index = index;
                        file.$state = function () {
                            return data.state();
                        };
                        file.$processing = function () {
                            return data.processing();
                        };
                        file.$progress = function () {
                            return data.progress();
                        };
                        file.$response = function () {
                            return data.response();
                        };
                        file.$formData = function (formData) {
                            data.formData = formData
                        };
                    });
                    file.$submit = function () {
                        if (!file.error) {
                            return data.submit();
                        }
                    };
                    file.$cancel = function () {
                        return data.abort();
                    };
                },
                $config;
            $config = this.defaults = {
                handleResponse: function (e, data) {
                    var files = data.result && data.result.files;
                    if (files) {
                        data.scope.replace(data.files, files);
                    } else if (data.errorThrown ||
                            data.textStatus === 'error') {
                        data.files[0].error = data.errorThrown ||
                            data.textStatus;
                    }
                },
                add: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var scope = data.scope,
                        filesCopy = [];
                    angular.forEach(data.files, function (file) {
                        filesCopy.push(file);
                    });
                    scope.$apply(function () {
                        addFileMethods(scope, data);
                        var method = scope.option('prependFiles') ?
                                'unshift' : 'push';
                        Array.prototype[method].apply(scope.queue, data.files);
                    });
                    data.process(function () {
                        return scope.process(data);
                    }).always(function () {
                        scope.$apply(function () {
                            addFileMethods(scope, data);
                            scope.replace(filesCopy, data.files);
                        });
                    }).then(function () {
                        if ((scope.option('autoUpload') ||
                                data.autoUpload) &&
                                data.autoUpload !== false) {
                            data.submit();
                        }
                    });
                },
                progress: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    data.scope.$apply();
                },
                done: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var that = this;
                    data.scope.$apply(function () {
                        data.handleResponse.call(that, e, data);
                    });
                },
                fail: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var that = this,
                        scope = data.scope;
                    if (data.errorThrown === 'abort') {
                        scope.clear(data.files);
                        return;
                    }
                    scope.$apply(function () {
                        data.handleResponse.call(that, e, data);
                    });
                },
                stop: scopeEvalAsync,
                processstart: scopeEvalAsync,
                processstop: scopeEvalAsync,
                getNumberOfFiles: function () {
                    var scope = this.scope;
                    return scope.queue.length - scope.processing();
                },
                dataType: 'json',
                autoUpload: false
            };
            this.$get = [
                function () {
                    return {
                        defaults: $config
                    };
                }
            ];
        })

        // Format byte numbers to readable presentations:
        .provider('formatFileSizeFilter', function () {
            var $config = {
                // Byte units following the IEC format
                // http://en.wikipedia.org/wiki/Kilobyte
                units: [
                    {size: 1000000000, suffix: ' GB'},
                    {size: 1000000, suffix: ' MB'},
                    {size: 1000, suffix: ' KB'}
                ]
            };
            this.defaults = $config;
            this.$get = function () {
                return function (bytes) {
                    if (!angular.isNumber(bytes)) {
                        return '';
                    }
                    var unit = true,
                        i = 0,
                        prefix,
                        suffix;
                    while (unit) {
                        unit = $config.units[i];
                        prefix = unit.prefix || '';
                        suffix = unit.suffix || '';
                        if (i === $config.units.length - 1 || bytes >= unit.size) {
                            return prefix + (bytes / unit.size).toFixed(2) + suffix;
                        }
                        i += 1;
                    }
                };
            };
        })

        // The FileUploadController initializes the fileupload widget and
        // provides scope methods to control the File Upload functionality:
        .controller('FileUploadController', [
            '$scope', '$element', '$attrs', '$window', 'fileUpload',
            function ($scope, $element, $attrs, $window, fileUpload) {
                var uploadMethods = {
                    progress: function () {
                        return $element.fileupload('progress');
                    },
                    active: function () {
                        return $element.fileupload('active');
                    },
                    option: function (option, data) {
                        if (arguments.length === 1) {
                            return $element.fileupload('option', option);
                        }
                        $element.fileupload('option', option, data);
                    },
                    add: function (data) {
                        return $element.fileupload('add', data);
                    },
                    send: function (data) {
                        return $element.fileupload('send', data);
                    },
                    process: function (data) {
                        return $element.fileupload('process', data);
                    },
                    processing: function (data) {
                        return $element.fileupload('processing', data);
                    }
                };
                $scope.disabled = !$window.jQuery.support.fileInput;
                $scope.queue = $scope.queue || [];
                $scope.clear = function (files) {
                    var queue = this.queue,
                        i = queue.length,
                        file = files,
                        length = 1;
                    if (angular.isArray(files)) {
                        file = files[0];
                        length = files.length;
                    }
                    while (i) {
                        i -= 1;
                        if (queue[i] === file) {
                            return queue.splice(i, length);
                        }
                    }
                };
                $scope.replace = function (oldFiles, newFiles) {
                    var queue = this.queue,
                        file = oldFiles[0],
                        i,
                        j;
                    for (i = 0; i < queue.length; i += 1) {
                        if (queue[i] === file) {
                            for (j = 0; j < newFiles.length; j += 1) {
                                queue[i + j] = newFiles[j];
                            }
                            return;
                        }
                    }
                };
                $scope.applyOnQueue = function (method) {
                    var list = this.queue.slice(0),
                        i,
                        file;
                    for (i = 0; i < list.length; i += 1) {
                        file = list[i];
                        if (file[method]) {
                            file[method]();
                        }
                    }
                };
                $scope.submit = function () {
                    this.applyOnQueue('$submit');
                };
                $scope.cancel = function () {
                    this.applyOnQueue('$cancel');
                };
                // Add upload methods to the scope:
                angular.extend($scope, uploadMethods);
                // The fileupload widget will initialize with
                // the options provided via "data-"-parameters,
                // as well as those given via options object:
                $element.fileupload(angular.extend(
                    {scope: $scope},
                    fileUpload.defaults
                )).on('fileuploadadd', function (e, data) {
                    data.scope = $scope;
                }).on('fileuploadfail', function (e, data) {
                    if (data.errorThrown === 'abort') {
                        return;
                    }
                    if (data.dataType &&
                            data.dataType.indexOf('json') === data.dataType.length - 4) {
                        try {
                            data.result = angular.fromJson(data.jqXHR.responseText);
                        } catch (ignore) {}
                    }
                }).on([
                    'fileuploadadd',
                    'fileuploadsubmit',
                    'fileuploadsend',
                    'fileuploaddone',
                    'fileuploadfail',
                    'fileuploadalways',
                    'fileuploadprogress',
                    'fileuploadprogressall',
                    'fileuploadstart',
                    'fileuploadstop',
                    'fileuploadchange',
                    'fileuploadpaste',
                    'fileuploaddrop',
                    'fileuploaddragover',
                    'fileuploadchunksend',
                    'fileuploadchunkdone',
                    'fileuploadchunkfail',
                    'fileuploadchunkalways',
                    'fileuploadprocessstart',
                    'fileuploadprocess',
                    'fileuploadprocessdone',
                    'fileuploadprocessfail',
                    'fileuploadprocessalways',
                    'fileuploadprocessstop'
                ].join(' '), function (e, data) {
                    if ($scope.$emit(e.type, data).defaultPrevented) {
                        e.preventDefault();
                    }
                }).on('remove', function () {
                    // Remove upload methods from the scope,
                    // when the widget is removed:
                    var method;
                    for (method in uploadMethods) {
                        if (uploadMethods.hasOwnProperty(method)) {
                            delete $scope[method];
                        }
                    }
                });
                // Observe option changes:
                $scope.$watch(
                    $attrs.fileUpload,
                    function (newOptions) {
                        if (newOptions) {
                            $element.fileupload('option', newOptions);
                        }
                    }
                );
            }
        ])

        // Provide File Upload progress feedback:
        .controller('FileUploadProgressController', [
            '$scope', '$attrs', '$parse',
            function ($scope, $attrs, $parse) {
                var fn = $parse($attrs.fileUploadProgress),
                    update = function () {
                        var progress = fn($scope);
                        if (!progress || !progress.total) {
                            return;
                        }
                        $scope.num = Math.floor(
                            progress.loaded / progress.total * 100
                        );
                    };
                update();
                $scope.$watch(
                    $attrs.fileUploadProgress + '.loaded',
                    function (newValue, oldValue) {
                        if (newValue !== oldValue) {
                            update();
                        }
                    }
                );
            }
        ])

        // Display File Upload previews:
        .controller('FileUploadPreviewController', [
            '$scope', '$element', '$attrs',
            function ($scope, $element, $attrs) {
                $scope.$watch(
                    $attrs.fileUploadPreview + '.preview',
                    function (preview) {
                        $element.empty();
                        if (preview) {
                            $element.append(preview);
                        }
                    }
                );
            }
        ])

        .directive('fileUpload', function () {
            return {
                controller: 'FileUploadController',
                scope: true
            };
        })

        .directive('fileUploadProgress', function () {
            return {
                controller: 'FileUploadProgressController',
                scope: true
            };
        })

        .directive('fileUploadPreview', function () {
            return {
                controller: 'FileUploadPreviewController'
            };
        })

        // Enhance the HTML5 download attribute to
        // allow drag&drop of files to the desktop:
        .directive('download', function () {
            return function (scope, elm) {
                elm.on('dragstart', function (e) {
                    try {
                        e.originalEvent.dataTransfer.setData(
                            'DownloadURL',
                            [
                                'application/octet-stream',
                                elm.prop('download'),
                                elm.prop('href')
                            ].join(':')
                        );
                    } catch (ignore) {}
                });
            };
        });

}));
<commit_msg>Patching fileupload-angular component<commit_after>/*
 * jQuery File Upload AngularJS Plugin 2.2.0
 * https://github.com/blueimp/jQuery-File-Upload
 *
 * Copyright 2013, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * http://www.opensource.org/licenses/MIT
 */

/* jshint nomen:false */
/* global define, angular */

(function (factory) {
    'use strict';
    if (typeof define === 'function' && define.amd) {
        // Register as an anonymous AMD module:
        define([
            'jquery',
            'angular',
            './jquery.fileupload-image',
            './jquery.fileupload-audio',
            './jquery.fileupload-video',
            './jquery.fileupload-validate'
        ], factory);
    } else {
        factory();
    }
}(function () {
    'use strict';

    angular.module('blueimp.fileupload', [])

        // The fileUpload service provides configuration options
        // for the fileUpload directive and default handlers for
        // File Upload events:
        .provider('fileUpload', function () {
            var scopeEvalAsync = function (expression) {
                    var scope = angular.element(this)
                            .fileupload('option', 'scope');
                    // Schedule a new $digest cycle if not already inside of one
                    // and evaluate the given expression:
                    scope.$evalAsync(expression);
                },
                addFileMethods = function (scope, data) {
                    var files = data.files,
                        file = files[0];
                    angular.forEach(files, function (file, index) {
                        file._index = index;
                        file.$state = function () {
                            return data.state();
                        };
                        file.$processing = function () {
                            return data.processing();
                        };
                        file.$progress = function () {
                            return data.progress();
                        };
                        file.$response = function () {
                            return data.response();
                        };
                        file.$formData = function (formData) {
                            data.formData = formData
                        };
                    });
                    file.$submit = function () {
                        if (!file.error) {
                            return data.submit();
                        }
                    };
                    file.$cancel = function () {
                        return data.abort();
                    };
                },
                $config;
            $config = this.defaults = {
                handleResponse: function (e, data) {
                    var files = data.result && data.result.files;
                    if (files) {
                        data.scope.replace(data.files, files);
                    } else if (data.errorThrown ||
                            data.textStatus === 'error') {
                        data.files[0].error = data.errorThrown ||
                            data.textStatus;
                    }
                },
                add: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var scope = data.scope,
                        filesCopy = [];
                    angular.forEach(data.files, function (file) {
                        filesCopy.push(file);
                    });
                    scope.$apply(function () {
                        addFileMethods(scope, data);
                        var method = scope.option('prependFiles') ?
                                'unshift' : 'push';
                        Array.prototype[method].apply(scope.queue, data.files);
                    });
                    data.process(function () {
                        return scope.process(data);
                    }).always(function () {
                        scope.$apply(function () {
                            addFileMethods(scope, data);
                            scope.replace(filesCopy, data.files);
                        });
                    }).then(function () {
                        if ((scope.option('autoUpload') ||
                                data.autoUpload) &&
                                data.autoUpload !== false) {
                            data.submit();
                        }
                    });
                },
                progress: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    data.scope.$apply();
                },
                done: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var that = this;
                    data.scope.$apply(function () {
                        data.handleResponse.call(that, e, data);
                    });
                },
                fail: function (e, data) {
                    if (e.isDefaultPrevented()) {
                        return false;
                    }
                    var that = this,
                        scope = data.scope;
                    if (data.errorThrown === 'abort') {
                        scope.clear(data.files);
                        return;
                    }
                    scope.$apply(function () {
                        data.handleResponse.call(that, e, data);
                    });
                },
                stop: scopeEvalAsync,
                processstart: scopeEvalAsync,
                processstop: scopeEvalAsync,
                getNumberOfFiles: function () {
                    var scope = this.scope;
                    return scope.queue.length - scope.processing();
                },
                dataType: 'json',
                autoUpload: false
            };
            this.$get = [
                function () {
                    return {
                        defaults: $config
                    };
                }
            ];
        })

        // Format byte numbers to readable presentations:
        .provider('formatFileSizeFilter', function () {
            var $config = {
                // Byte units following the IEC format
                // http://en.wikipedia.org/wiki/Kilobyte
                units: [
                    {size: 1000000000, suffix: ' GB'},
                    {size: 1000000, suffix: ' MB'},
                    {size: 1000, suffix: ' KB'}
                ]
            };
            this.defaults = $config;
            this.$get = function () {
                return function (bytes) {
                    if (!angular.isNumber(bytes)) {
                        return '';
                    }
                    var unit = true,
                        i = 0,
                        prefix,
                        suffix;
                    while (unit) {
                        unit = $config.units[i];
                        prefix = unit.prefix || '';
                        suffix = unit.suffix || '';
                        if (i === $config.units.length - 1 || bytes >= unit.size) {
                            return prefix + (bytes / unit.size).toFixed(2) + suffix;
                        }
                        i += 1;
                    }
                };
            };
        })

        // The FileUploadController initializes the fileupload widget and
        // provides scope methods to control the File Upload functionality:
        .controller('FileUploadController', [
            '$scope', '$element', '$attrs', '$window', 'fileUpload',
            function ($scope, $element, $attrs, $window, fileUpload) {
                var uploadMethods = {
                    progress: function () {
                        return $element.fileupload('progress');
                    },
                    active: function () {
                        return $element.fileupload('active');
                    },
                    option: function (option, data) {
                        if (arguments.length === 1) {
                            return $element.fileupload('option', option);
                        }
                        $element.fileupload('option', option, data);
                    },
                    add: function (data) {
                        return $element.fileupload('add', data);
                    },
                    send: function (data) {
                        return $element.fileupload('send', data);
                    },
                    process: function (data) {
                        return $element.fileupload('process', data);
                    },
                    processing: function (data) {
                        return $element.fileupload('processing', data);
                    }
                };
                $scope.disabled = !$window.jQuery.support.fileInput;
                $scope.queue = $scope.queue || [];
                $scope.clear = function (files) {
                    var queue = this.queue,
                        i = queue.length,
                        file = files,
                        length = 1;
                    if (angular.isArray(files)) {
                        file = files[0];
                        length = files.length;
                    }
                    while (i) {
                        i -= 1;
                        if (queue[i] === file) {
                            return queue.splice(i, length);
                        }
                    }
                };
                $scope.replace = function (oldFiles, newFiles) {
                    var queue = this.queue,
                        file = oldFiles[0],
                        i,
                        j;
                    for (i = 0; i < queue.length; i += 1) {
                        if (queue[i] === file) {
                            for (j = 0; j < newFiles.length; j += 1) {
                                queue[i + j] = newFiles[j];
                            }
                            return;
                        }
                    }
                };
                $scope.applyOnQueue = function (method) {
                    var list = this.queue.slice(0),
                        i,
                        file;
                    for (i = 0; i < list.length; i += 1) {
                        file = list[i];
                        if (file[method]) {
                            file[method]();
                        }
                    }
                };
                $scope.submit = function () {
                    this.applyOnQueue('$submit');
                };
                $scope.cancel = function () {
                    this.applyOnQueue('$cancel');
                };
                // Add upload methods to the scope:
                angular.extend($scope, uploadMethods);
                // The fileupload widget will initialize with
                // the options provided via "data-"-parameters,
                // as well as those given via options object:
                $element.fileupload(angular.extend(
                    {scope: $scope},
                    fileUpload.defaults
                )).on('fileuploadadd', function (e, data) {
                    data.scope = $scope;
                }).on('fileuploadfail', function (e, data) {
                    if (data.errorThrown === 'abort') {
                        return;
                    }
                    if (data.dataType &&
                            data.dataType.indexOf('json') === data.dataType.length - 4) {
                        try {
                            data.result = angular.fromJson(data.jqXHR.responseText);
                        } catch (ignore) {}
                    }
                }).on([
                    'fileuploadadd',
                    'fileuploadsubmit',
                    'fileuploadsend',
                    'fileuploaddone',
                    'fileuploadfail',
                    'fileuploadalways',
                    'fileuploadprogress',
                    'fileuploadprogressall',
                    'fileuploadstart',
                    'fileuploadstop',
                    'fileuploadchange',
                    'fileuploadpaste',
                    'fileuploaddrop',
                    'fileuploaddragover',
                    'fileuploadchunksend',
                    'fileuploadchunkdone',
                    'fileuploadchunkfail',
                    'fileuploadchunkalways',
                    'fileuploadprocessstart',
                    'fileuploadprocess',
                    'fileuploadprocessdone',
                    'fileuploadprocessfail',
                    'fileuploadprocessalways',
                    'fileuploadprocessstop'
                ].join(' '), function (e, data) {
                    if ($scope.$emit(e.type, data).defaultPrevented) {
                        e.preventDefault();
                    }
                }).on('remove', function () {
                    // Remove upload methods from the scope,
                    // when the widget is removed:
                    var method;
                    for (method in uploadMethods) {
                        if (uploadMethods.hasOwnProperty(method)) {
                            delete $scope[method];
                        }
                    }
                });
                // Observe option changes:
                $scope.$watch(
                    $attrs.fileUpload,
                    function (newOptions) {
                        if (newOptions) {
                            $element.fileupload('option', newOptions);
                        }
                    }
                );
            }
        ])

        // Provide File Upload progress feedback:
        .controller('FileUploadProgressController', [
            '$scope', '$attrs', '$parse',
            function ($scope, $attrs, $parse) {
                var fn = $parse($attrs.fileUploadProgress),
                    update = function () {
                        var progress = fn($scope);
                        if (!progress || !progress.total) {
                            return;
                        }
                        $scope.num = Math.floor(
                            progress.loaded / progress.total * 100
                        );
                    };
                update();
                $scope.$watch(
                    $attrs.fileUploadProgress + '.loaded',
                    function (newValue, oldValue) {
                        if (newValue !== oldValue) {
                            update();
                        }
                    }
                );
            }
        ])

        // Display File Upload previews:
        .controller('FileUploadPreviewController', [
            '$scope', '$element', '$attrs',
            function ($scope, $element, $attrs) {
                $scope.$watch(
                    $attrs.fileUploadPreview + '.preview',
                    function (preview) {
                        $element.empty();
                        if (preview) {
                            $element.append(preview);
                        }
                    }
                );
            }
        ])

        .directive('fileUpload', function () {
            return {
                controller: 'FileUploadController'
            };
        })

        .directive('fileUploadProgress', function () {
            return {
                controller: 'FileUploadProgressController',
                scope: true
            };
        })

        .directive('fileUploadPreview', function () {
            return {
                controller: 'FileUploadPreviewController'
            };
        })

        // Enhance the HTML5 download attribute to
        // allow drag&drop of files to the desktop:
        .directive('download', function () {
            return function (scope, elm) {
                elm.on('dragstart', function (e) {
                    try {
                        e.originalEvent.dataTransfer.setData(
                            'DownloadURL',
                            [
                                'application/octet-stream',
                                elm.prop('download'),
                                elm.prop('href')
                            ].join(':')
                        );
                    } catch (ignore) {}
                });
            };
        });

}));
<|endoftext|>


<commit_before>import * as softReturn from '@tryghost/kg-parser-plugins/lib/cards/softReturn';
import Component from '@ember/component';
import Editor from 'mobiledoc-kit/editor/editor';
import classic from 'ember-classic-decorator';
import cleanBasicHtml from '@tryghost/kg-clean-basic-html';
import defaultAtoms from '../options/atoms';
import registerKeyCommands, {BASIC_TEXTAREA_KEY_COMMANDS} from '../options/key-commands';
import validator from 'validator';
import {BLANK_DOC, MOBILEDOC_VERSION} from './koenig-editor';
import {DRAG_DISABLED_DATA_ATTR} from '../lib/dnd/constants';
import {action, computed} from '@ember/object';
import {arrayToMap} from './koenig-editor';
import {assign} from '@ember/polyfills';
import {getContentFromPasteEvent} from 'mobiledoc-kit/utils/parse-utils';
import {getLinkMarkupFromRange} from '../utils/markup-utils';
import {registerBasicTextareaTextExpansions} from '../options/text-expansions';
import {run} from '@ember/runloop';

// TODO: extract core to share functionality between this and `{{koenig-editor}}`

const UNDO_DEPTH = 50;

// markups that should not be continued when typing and reverted to their
// text expansion style when backspacing over final char of markup
const SPECIAL_MARKUPS = {
    S: '~~',
    CODE: {
        char: '`',
        replace: false
    },
    SUP: '^',
    SUB: '~'
};

// if the cursor is at the end of one of our "special" markups that can only be
// toggled via markdown expansions then we want to ensure that the markup is
// removed from the edit state so that you can type without being stuck with
// the special formatting
function toggleSpecialFormatEditState(editor) {
    let {head, isCollapsed} = editor.range;
    if (isCollapsed) {
        Object.keys(SPECIAL_MARKUPS).forEach((tagName) => {
            tagName = tagName.toLowerCase();
            if (head.marker && head.marker.hasMarkup(tagName) && editor._editState.activeMarkups.findBy('tagName', tagName)) {
                let nextMarker = head.markerIn(1);
                if (!nextMarker || !nextMarker.hasMarkup(tagName)) {
                    // there is a bug somehwhere that means after pasting
                    // content the _editState can end up with multiple
                    // instances of the markup so we need to toggle all of them
                    editor._editState.activeMarkups.filterBy('tagName', tagName).forEach((markup) => {
                        editor._editState.toggleMarkupState(markup);
                    });
                }
            }
        });
    }
}

@classic
export default class KoenigBasicHtmlTextarea extends Component {
    // public attrs
    autofocus = false;
    html = null;
    placeholder = '';
    spellcheck = true;

    // internal properties
    activeMarkupTagNames = null;
    editor = null;
    linkRange = null;
    mobiledoc = null;
    selectedRange = null;

    // private properties
    _hasFocus = false;
    _lastMobiledoc = null;
    _startedRunLoop = false;

    // closure actions
    willCreateEditor() {}
    didCreateEditor() {}
    onChange() {}
    onFocus() {}
    onBlur() {}

    /* computed properties -------------------------------------------------- */

    // merge in named options with any passed in `options` property data-bag
    @computed('html')
    get editorOptions() {
        let options = this.options || {};
        let atoms = this.atoms || [];
        let cards = this.cards || [];

        // add our default atoms and cards, we want the defaults to be first so
        // that they can be overridden by any passed-in atoms or cards.
        // Use Array.concat to avoid modifying any passed in array references
        atoms = defaultAtoms.concat(atoms);

        return assign({
            html: this.html || '',
            placeholder: this.placeholder,
            spellcheck: this.spellcheck,
            autofocus: this.autofocus,
            cards,
            atoms,
            unknownCardHandler() {},
            unknownAtomHandler() {}
        }, options);
    }

    /* lifecycle hooks ------------------------------------------------------ */

    init() {
        super.init(...arguments);

        this.SPECIAL_MARKUPS = SPECIAL_MARKUPS;
    }

    didReceiveAttrs() {
        super.didReceiveAttrs(...arguments);

        // reset local mobiledoc if html has been changed upstream so that
        // the html will be re-parsed by the mobiledoc-kit editor
        if (this.html !== this._lastHtml && this.html !== this.getCleanHTML()) {
            this.set('mobiledoc', null);
        }
        this._lastHtml = this.html;
    }

    willRender() {
        super.willRender(...arguments);

        let mobiledoc = this.mobiledoc;

        if (!mobiledoc && !this.html) {
            mobiledoc = BLANK_DOC;
        }

        let mobiledocIsSame =
            (this._lastMobiledoc && this._lastMobiledoc === mobiledoc);
        let isEditingDisabledIsSame =
            this._lastIsEditingDisabled === this.isEditingDisabled;

        // no change to mobiledoc, no need to recreate the editor
        if (mobiledocIsSame && isEditingDisabledIsSame) {
            return;
        }

        // update our internal references
        this._lastIsEditingDisabled = this.isEditingDisabled;

        // trigger the willCreateEditor closure action
        this.willCreateEditor();

        // teardown any old editor that might be around
        let editor = this.editor;
        if (editor) {
            editor.destroy();
        }

        // create a new editor
        let editorOptions = this.editorOptions;
        editorOptions.mobiledoc = mobiledoc;
        editorOptions.showLinkTooltips = false;
        editorOptions.undoDepth = UNDO_DEPTH;
        editorOptions.parserPlugins = [softReturn.fromBr()];

        editor = new Editor(editorOptions);

        registerKeyCommands(editor, this, BASIC_TEXTAREA_KEY_COMMANDS);
        registerBasicTextareaTextExpansions(editor, this);

        // set up editor hooks
        editor.willRender(() => {
            // The editor's render/rerender will happen after this `editor.willRender`,
            // so we explicitly start a runloop here if there is none, so that the
            // add/remove card hooks happen inside a runloop.
            // When pasting text that gets turned into a card, for example,
            // the add card hook would run outside the runloop if we didn't begin a new
            // one now.
            if (!run.currentRunLoop) {
                this._startedRunLoop = true;
                run.begin();
            }
        });

        editor.didRender(() => {
            // if we had explicitly started a runloop in `editor.willRender`,
            // we must explicitly end it here
            if (this._startedRunLoop) {
                this._startedRunLoop = false;
                run.end();
            }
        });

        editor.didUpdatePost((postEditor) => {
            run.join(() => {
                this.didUpdatePost(postEditor);
            });
        });

        editor.postDidChange(() => {
            run.join(() => {
                this.postDidChange(editor);
            });
        });

        editor.cursorDidChange(() => {
            run.join(() => {
                this.cursorDidChange(editor);
            });
        });

        editor.inputModeDidChange(() => {
            if (this.isDestroyed) {
                return;
            }
            run.join(() => {
                this.inputModeDidChange(editor);
            });
        });

        if (this.isEditingDisabled) {
            editor.disableEditing();
        }

        // update mobiledoc reference to match initial editor state from parsed
        // html. We use this value to compare on re-renders in case we need to
        // re-parse from html
        this.mobiledoc = editor.serialize(MOBILEDOC_VERSION);
        this._lastMobiledoc = this.mobiledoc;

        this.set('editor', editor);
        this.didCreateEditor(editor);
    }

    didInsertElement() {
        super.didInsertElement(...arguments);
        let editorElement = this.element.querySelector('[data-kg="editor"]');

        this._pasteHandler = run.bind(this, this.handlePaste);
        editorElement.addEventListener('paste', this._pasteHandler);

        this.element.dataset[DRAG_DISABLED_DATA_ATTR] = 'true';
    }

    // our ember component has rendered, now we need to render the mobiledoc
    // editor itself if necessary
    didRender() {
        super.didRender(...arguments);
        let {editor} = this;
        if (!editor.hasRendered) {
            let editorElement = this.element.querySelector('[data-kg="editor"]');
            this._isRenderingEditor = true;
            editor.render(editorElement);
            this._isRenderingEditor = false;
        }
    }

    willDestroyElement() {
        super.willDestroyElement(...arguments);

        let editorElement = this.element.querySelector('[data-kg="editor"]');
        editorElement.removeEventListener('paste', this._pasteHandler);

        this.editor.destroy();
    }

    @action
    toggleMarkup(markupTagName, postEditor) {
        (postEditor || this.editor).toggleMarkup(markupTagName);
    }

    // range should be set to the full extent of the selection or the
    // appropriate <a> markup. If there's a selection when the link edit
    // component renders it will re-select when finished which should
    // trigger the normal toolbar
    @action
    editLink(range) {
        let linkMarkup = getLinkMarkupFromRange(range);
        if ((!range.isCollapsed || linkMarkup) && range.headSection.isMarkerable) {
            this.set('linkRange', range);
        }
    }

    @action
    cancelEditLink() {
        this.set('linkRange', null);
    }

    /* ember event handlers --------------------------------------------------*/

    // handle focusin/focusout at the component level so that we don't trigger blur
    // actions when clicking on toolbar buttons
    focusIn(event) {
        if (!this._hasFocus) {
            this._hasFocus = true;
            run.scheduleOnce('actions', this, this.onFocus, event);
        }
    }

    focusOut(event) {
        if (!event.relatedTarget || !this.element.contains(event.relatedTarget)) {
            this._hasFocus = false;
            run.scheduleOnce('actions', this, this.onBlur, event);
        }
    }

    /* custom event handlers ------------------------------------------------ */

    handlePaste(event) {
        let {editor, editor: {range}} = this;
        let {text} = getContentFromPasteEvent(event);

        if (!editor.cursor.isAddressable(event.target)) {
            return;
        }

        if (text && validator.isURL(text)) {
            // if we have a text selection, make that selection a link
            if (range && !range.isCollapsed && range.headSection === range.tailSection && range.headSection.isMarkerable) {
                let linkMarkup = editor.builder.createMarkup('a', {href: text});
                editor.run((postEditor) => {
                    postEditor.addMarkupToRange(range, linkMarkup);
                });
                editor.selectRange(range.tail);

                // prevent mobiledoc's default paste event handler firing
                event.preventDefault();
                event.stopImmediatePropagation();
                return;
            }
        }
    }

    /* mobiledoc event handlers ----------------------------------------------*/

    // manipulate mobiledoc content before committing changes
    // - only one section
    // - first section must be a markerable section
    // - if first section is a list, grab the content of the first list item
    didUpdatePost(postEditor) {
        let {editor, editor: {post}} = postEditor;

        // remove any other formatting from code formats
        let markers = [];
        try {
            markers = post.markersContainedByRange(post.toRange());
        } catch (e) {
            // post.toRange() can fail if a list item was just removed
            // TODO: mobiledoc-kit bug?
        }
        markers.forEach((marker) => {
            let {markups} = marker;
            if (markups.length > 1 && marker.hasMarkup('code')) {
                markups.rejectBy('tagName', 'code').forEach((markup) => {
                    marker.removeMarkup(markup);
                });
            }
        });

        // remove any non-markerable/non-list sections
        post.sections.forEach((section) => {
            if (!section.isMarkerable && !section.isListSection) {
                let reposition = section === editor.activeSection;
                postEditor.removeSection(section);
                if (reposition) {
                    postEditor.setRange(post.sections.head.tailPosition());
                }
            }
        });
    }

    postDidChange() {
        // trigger closure action
        this.onChange(this.getCleanHTML());
    }

    cursorDidChange(editor) {
        // if we have `code` or ~strike~ formatting to the left but not the right
        // then toggle the formatting - these formats should only be creatable
        // through the text expansions
        toggleSpecialFormatEditState(editor);

        // pass the selected range through to the toolbar + menu components
        this.set('selectedRange', editor.range);
    }

    // fired when the active section(s) or markup(s) at the current cursor
    // position or selection have changed. We use this event to update the
    // activeMarkup/section tag lists which control button states in our popup
    // toolbar
    inputModeDidChange(editor) {
        let markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));

        // On keyboard cursor movement our `cursorDidChange` toggle for special
        // formats happens before mobiledoc's readstate updates the edit states
        // so we have to re-do it here
        // TODO: can we make the event order consistent in mobiledoc-kit?
        toggleSpecialFormatEditState(editor);

        // Avoid updating this component's properties synchronously while
        // rendering the editor (after rendering the component) because it
        // causes Ember to display deprecation warnings
        if (this._isRenderingEditor) {
            run.schedule('afterRender', () => {
                this.set('activeMarkupTagNames', markupTags);
            });
        } else {
            this.set('activeMarkupTagNames', markupTags);
        }
    }

    /* private methods -------------------------------------------------------*/

    // rather than parsing mobiledoc to HTML we can grab the HTML directly from
    // inside the editor element because we should only be dealing with
    // inline markup that directly maps to HTML elements
    getCleanHTML() {
        if (this.editor && this.editor.element) {
            if (this.editor.element.classList.contains('__has-no-content')) {
                // Removes the default editor placeholder
                return '';
            }
            return cleanBasicHtml(this.editor.element.innerHTML, {allowBr: true});
        }
    }
}
<commit_msg>🐛 Fixed paste into product card descriptions not stripping header formatting<commit_after>import * as softReturn from '@tryghost/kg-parser-plugins/lib/cards/softReturn';
import Component from '@ember/component';
import Editor from 'mobiledoc-kit/editor/editor';
import classic from 'ember-classic-decorator';
import cleanBasicHtml from '@tryghost/kg-clean-basic-html';
import defaultAtoms from '../options/atoms';
import registerKeyCommands, {BASIC_TEXTAREA_KEY_COMMANDS} from '../options/key-commands';
import validator from 'validator';
import {BLANK_DOC, MOBILEDOC_VERSION} from './koenig-editor';
import {DRAG_DISABLED_DATA_ATTR} from '../lib/dnd/constants';
import {action, computed} from '@ember/object';
import {arrayToMap} from './koenig-editor';
import {assign} from '@ember/polyfills';
import {getContentFromPasteEvent} from 'mobiledoc-kit/utils/parse-utils';
import {getLinkMarkupFromRange} from '../utils/markup-utils';
import {registerBasicTextareaTextExpansions} from '../options/text-expansions';
import {run} from '@ember/runloop';

// TODO: extract core to share functionality between this and `{{koenig-editor}}`

const UNDO_DEPTH = 50;

// markups that should not be continued when typing and reverted to their
// text expansion style when backspacing over final char of markup
const SPECIAL_MARKUPS = {
    S: '~~',
    CODE: {
        char: '`',
        replace: false
    },
    SUP: '^',
    SUB: '~'
};

// if the cursor is at the end of one of our "special" markups that can only be
// toggled via markdown expansions then we want to ensure that the markup is
// removed from the edit state so that you can type without being stuck with
// the special formatting
function toggleSpecialFormatEditState(editor) {
    let {head, isCollapsed} = editor.range;
    if (isCollapsed) {
        Object.keys(SPECIAL_MARKUPS).forEach((tagName) => {
            tagName = tagName.toLowerCase();
            if (head.marker && head.marker.hasMarkup(tagName) && editor._editState.activeMarkups.findBy('tagName', tagName)) {
                let nextMarker = head.markerIn(1);
                if (!nextMarker || !nextMarker.hasMarkup(tagName)) {
                    // there is a bug somehwhere that means after pasting
                    // content the _editState can end up with multiple
                    // instances of the markup so we need to toggle all of them
                    editor._editState.activeMarkups.filterBy('tagName', tagName).forEach((markup) => {
                        editor._editState.toggleMarkupState(markup);
                    });
                }
            }
        });
    }
}

@classic
export default class KoenigBasicHtmlTextarea extends Component {
    // public attrs
    autofocus = false;
    html = null;
    placeholder = '';
    spellcheck = true;

    // internal properties
    activeMarkupTagNames = null;
    editor = null;
    linkRange = null;
    mobiledoc = null;
    selectedRange = null;

    // private properties
    _hasFocus = false;
    _lastMobiledoc = null;
    _startedRunLoop = false;

    // closure actions
    willCreateEditor() {}
    didCreateEditor() {}
    onChange() {}
    onFocus() {}
    onBlur() {}

    /* computed properties -------------------------------------------------- */

    // merge in named options with any passed in `options` property data-bag
    @computed('html')
    get editorOptions() {
        let options = this.options || {};
        let atoms = this.atoms || [];
        let cards = this.cards || [];

        // add our default atoms and cards, we want the defaults to be first so
        // that they can be overridden by any passed-in atoms or cards.
        // Use Array.concat to avoid modifying any passed in array references
        atoms = defaultAtoms.concat(atoms);

        return assign({
            html: this.html || '',
            placeholder: this.placeholder,
            spellcheck: this.spellcheck,
            autofocus: this.autofocus,
            cards,
            atoms,
            unknownCardHandler() {},
            unknownAtomHandler() {}
        }, options);
    }

    /* lifecycle hooks ------------------------------------------------------ */

    init() {
        super.init(...arguments);

        this.SPECIAL_MARKUPS = SPECIAL_MARKUPS;
    }

    didReceiveAttrs() {
        super.didReceiveAttrs(...arguments);

        // reset local mobiledoc if html has been changed upstream so that
        // the html will be re-parsed by the mobiledoc-kit editor
        if (this.html !== this._lastHtml && this.html !== this.getCleanHTML()) {
            this.set('mobiledoc', null);
        }
        this._lastHtml = this.html;
    }

    willRender() {
        super.willRender(...arguments);

        let mobiledoc = this.mobiledoc;

        if (!mobiledoc && !this.html) {
            mobiledoc = BLANK_DOC;
        }

        let mobiledocIsSame =
            (this._lastMobiledoc && this._lastMobiledoc === mobiledoc);
        let isEditingDisabledIsSame =
            this._lastIsEditingDisabled === this.isEditingDisabled;

        // no change to mobiledoc, no need to recreate the editor
        if (mobiledocIsSame && isEditingDisabledIsSame) {
            return;
        }

        // update our internal references
        this._lastIsEditingDisabled = this.isEditingDisabled;

        // trigger the willCreateEditor closure action
        this.willCreateEditor();

        // teardown any old editor that might be around
        let editor = this.editor;
        if (editor) {
            editor.destroy();
        }

        // create a new editor
        let editorOptions = this.editorOptions;
        editorOptions.mobiledoc = mobiledoc;
        editorOptions.showLinkTooltips = false;
        editorOptions.undoDepth = UNDO_DEPTH;
        editorOptions.parserPlugins = [softReturn.fromBr()];

        editor = new Editor(editorOptions);

        registerKeyCommands(editor, this, BASIC_TEXTAREA_KEY_COMMANDS);
        registerBasicTextareaTextExpansions(editor, this);

        // set up editor hooks
        editor.willRender(() => {
            // The editor's render/rerender will happen after this `editor.willRender`,
            // so we explicitly start a runloop here if there is none, so that the
            // add/remove card hooks happen inside a runloop.
            // When pasting text that gets turned into a card, for example,
            // the add card hook would run outside the runloop if we didn't begin a new
            // one now.
            if (!run.currentRunLoop) {
                this._startedRunLoop = true;
                run.begin();
            }
        });

        editor.didRender(() => {
            // if we had explicitly started a runloop in `editor.willRender`,
            // we must explicitly end it here
            if (this._startedRunLoop) {
                this._startedRunLoop = false;
                run.end();
            }
        });

        editor.didUpdatePost((postEditor) => {
            run.join(() => {
                this.didUpdatePost(postEditor);
            });
        });

        editor.postDidChange(() => {
            run.join(() => {
                this.postDidChange(editor);
            });
        });

        editor.cursorDidChange(() => {
            run.join(() => {
                this.cursorDidChange(editor);
            });
        });

        editor.inputModeDidChange(() => {
            if (this.isDestroyed) {
                return;
            }
            run.join(() => {
                this.inputModeDidChange(editor);
            });
        });

        if (this.isEditingDisabled) {
            editor.disableEditing();
        }

        // update mobiledoc reference to match initial editor state from parsed
        // html. We use this value to compare on re-renders in case we need to
        // re-parse from html
        this.mobiledoc = editor.serialize(MOBILEDOC_VERSION);
        this._lastMobiledoc = this.mobiledoc;

        this.set('editor', editor);
        this.didCreateEditor(editor);
    }

    didInsertElement() {
        super.didInsertElement(...arguments);
        let editorElement = this.element.querySelector('[data-kg="editor"]');

        this._pasteHandler = run.bind(this, this.handlePaste);
        editorElement.addEventListener('paste', this._pasteHandler);

        this.element.dataset[DRAG_DISABLED_DATA_ATTR] = 'true';
    }

    // our ember component has rendered, now we need to render the mobiledoc
    // editor itself if necessary
    didRender() {
        super.didRender(...arguments);
        let {editor} = this;
        if (!editor.hasRendered) {
            let editorElement = this.element.querySelector('[data-kg="editor"]');
            this._isRenderingEditor = true;
            editor.render(editorElement);
            this._isRenderingEditor = false;
        }
    }

    willDestroyElement() {
        super.willDestroyElement(...arguments);

        let editorElement = this.element.querySelector('[data-kg="editor"]');
        editorElement.removeEventListener('paste', this._pasteHandler);

        this.editor.destroy();
    }

    @action
    toggleMarkup(markupTagName, postEditor) {
        (postEditor || this.editor).toggleMarkup(markupTagName);
    }

    // range should be set to the full extent of the selection or the
    // appropriate <a> markup. If there's a selection when the link edit
    // component renders it will re-select when finished which should
    // trigger the normal toolbar
    @action
    editLink(range) {
        let linkMarkup = getLinkMarkupFromRange(range);
        if ((!range.isCollapsed || linkMarkup) && range.headSection.isMarkerable) {
            this.set('linkRange', range);
        }
    }

    @action
    cancelEditLink() {
        this.set('linkRange', null);
    }

    /* ember event handlers --------------------------------------------------*/

    // handle focusin/focusout at the component level so that we don't trigger blur
    // actions when clicking on toolbar buttons
    focusIn(event) {
        if (!this._hasFocus) {
            this._hasFocus = true;
            run.scheduleOnce('actions', this, this.onFocus, event);
        }
    }

    focusOut(event) {
        if (!event.relatedTarget || !this.element.contains(event.relatedTarget)) {
            this._hasFocus = false;
            run.scheduleOnce('actions', this, this.onBlur, event);
        }
    }

    /* custom event handlers ------------------------------------------------ */

    handlePaste(event) {
        let {editor, editor: {range}} = this;
        let {text} = getContentFromPasteEvent(event);

        if (!editor.cursor.isAddressable(event.target)) {
            return;
        }

        if (text && validator.isURL(text)) {
            // if we have a text selection, make that selection a link
            if (range && !range.isCollapsed && range.headSection === range.tailSection && range.headSection.isMarkerable) {
                let linkMarkup = editor.builder.createMarkup('a', {href: text});
                editor.run((postEditor) => {
                    postEditor.addMarkupToRange(range, linkMarkup);
                });
                editor.selectRange(range.tail);

                // prevent mobiledoc's default paste event handler firing
                event.preventDefault();
                event.stopImmediatePropagation();
                return;
            }
        }
    }

    /* mobiledoc event handlers ----------------------------------------------*/

    // manipulate mobiledoc content before committing changes
    // - only one section
    // - first section must be a markerable section
    // - if first section is a list, grab the content of the first list item
    didUpdatePost(postEditor) {
        let {editor, editor: {post}} = postEditor;

        // remove any other formatting from code formats
        let markers = [];
        try {
            markers = post.markersContainedByRange(post.toRange());
        } catch (e) {
            // post.toRange() can fail if a list item was just removed
            // TODO: mobiledoc-kit bug?
        }

        markers.forEach((marker) => {
            let {markups} = marker;
            if (markups.length > 1 && marker.hasMarkup('code')) {
                markups.rejectBy('tagName', 'code').forEach((markup) => {
                    marker.removeMarkup(markup);
                });
            }
        });

        // remove any non-markerable/non-list sections
        post.sections.forEach((section) => {
            // headings are not supported so convert them to paragraphs
            if (section.isMarkerable) {
                section.tagName = 'p';
            }

            if (!section.isMarkerable && !section.isListSection) {
                let reposition = section === editor.activeSection;
                postEditor.removeSection(section);
                if (reposition) {
                    postEditor.setRange(post.sections.head.tailPosition());
                }
            }
        });
    }

    postDidChange() {
        // trigger closure action
        this.onChange(this.getCleanHTML());
    }

    cursorDidChange(editor) {
        // if we have `code` or ~strike~ formatting to the left but not the right
        // then toggle the formatting - these formats should only be creatable
        // through the text expansions
        toggleSpecialFormatEditState(editor);

        // pass the selected range through to the toolbar + menu components
        this.set('selectedRange', editor.range);
    }

    // fired when the active section(s) or markup(s) at the current cursor
    // position or selection have changed. We use this event to update the
    // activeMarkup/section tag lists which control button states in our popup
    // toolbar
    inputModeDidChange(editor) {
        let markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));

        // On keyboard cursor movement our `cursorDidChange` toggle for special
        // formats happens before mobiledoc's readstate updates the edit states
        // so we have to re-do it here
        // TODO: can we make the event order consistent in mobiledoc-kit?
        toggleSpecialFormatEditState(editor);

        // Avoid updating this component's properties synchronously while
        // rendering the editor (after rendering the component) because it
        // causes Ember to display deprecation warnings
        if (this._isRenderingEditor) {
            run.schedule('afterRender', () => {
                this.set('activeMarkupTagNames', markupTags);
            });
        } else {
            this.set('activeMarkupTagNames', markupTags);
        }
    }

    /* private methods -------------------------------------------------------*/

    // rather than parsing mobiledoc to HTML we can grab the HTML directly from
    // inside the editor element because we should only be dealing with
    // inline markup that directly maps to HTML elements
    getCleanHTML() {
        if (this.editor && this.editor.element) {
            if (this.editor.element.classList.contains('__has-no-content')) {
                // Removes the default editor placeholder
                return '';
            }
            return cleanBasicHtml(this.editor.element.innerHTML, {allowBr: true});
        }
    }
}
<|endoftext|>