openui5 icon indicating copy to clipboard operation
openui5 copied to clipboard

sap.m.Image src=*.svg support

Open hschaefer123 opened this issue 9 years ago • 15 comments

Hi guys, i am currently using svg images as Image sources.

To be able to apply styling via css the external svg needs to be converted as inline svg.

Currently this is a work in progress and i do something like

        jQuery('.svg-inject .sapMImg').each(function(){
            var $img = jQuery(this);
            var imgID = $img.attr("id");
            var imgDataSapUi = $img.attr("data-sap-ui");
            var imgRole = $img.attr("role");
            var imgAriaHidden = $img.attr("aria-hidden");
            var imgClass = $img.attr("class");
            var imgStyle = $img.attr("style");
            var imgURL = $img.attr("src");

            jQuery.get(imgURL, function(data) {
                // Get the SVG tag, ignore the rest
                var $svg = jQuery(data).find("svg");

                // Add replaced image's ID to the new SVG
                if(typeof imgID !== "undefined") {
                    $svg = $svg.attr("id", imgID);
                }
                if(typeof imgDataSapUi !== "undefined") {
                    $svg = $svg.attr("data-sap-ui", imgDataSapUi);
                }
                if(typeof imgRole !== "undefined") {
                    $svg = $svg.attr("role", imgRole);
                }
                if(typeof imgAriaHidden !== "undefined") {
                    $svg = $svg.attr("aria-hidden", imgAriaHidden);
                }
                // Add replaced image's classes to the new SVG
                if(typeof imgClass !== "undefined") {
                    $svg = $svg.attr("class", imgClass + " svg-inline");
                }
                // Add replaced image's styles to the new SVG
                if(typeof imgStyle !== "undefined") {
                    $svg = $svg.attr("style", imgStyle);
                }

                // Remove any invalid XML tags as per http://validator.w3.org
                $svg = $svg.removeAttr("xmlns:a");

                // Check if the viewport is set, if the viewport is not set the SVG wont't scale.
                if(!$svg.attr('viewBox') && $svg.attr('height') && $svg.attr('width')) {
                    $svg.attr('viewBox', '0 0 ' + $svg.attr('height') + ' ' + $svg.attr('width'))
                }

                // Replace image with new SVG
                $img.replaceWith($svg);
            }, 'xml');

There are also more experienced libs like https://github.com/robbue/jquery.svgInject availbale, which uses an inline cache for replacing img with svg.

This is only a WIP but still works. I just started to thing (and learn) about using SVG. Using it this way, the image will be replaced by an inline svg object.

You also need densityAware = false because svg is a vector and src should not load @2.svg

It would be great, if such feature could be integrated into the sap.m.image, maybe with an inline option by default to be able to use such SVGs also in the sap.m.ObjectHeader.

We use this feature inside out theme for company logo's, where the external src is ok.

Using SVGs as user placeholder images we like to control the fills, strokes, etc with colors inside our custom specific theme using $BaseColor, $AccentColor and such SVGs needs to be replaced as inline object svg.

What do you thing?

Regards, Holger

hschaefer123 avatar Dec 21 '15 08:12 hschaefer123

I played around with a custom widget

sap.ui.define([
    "sap/m/Image", 
    "3rd/jquery.svgInject"
    ],
    function (Image) {
    "use strict";

        var SvgImage = Image.extend("uniorg.m.SvgImage", {
            renderer: {}             
        });

        /**
        * After src change replace img.src pointing to external *.svg with embedded svg using custom version of svgInject
        * current issue : changing src by setSrc -> need to rerender svg to embedd changed markup!!!
        * src property binding will cause an rerender overhead (by design)!
        * @private
        */
        SvgImage.prototype._updateDomSrc = function() {
            if (Image.prototype._updateDomSrc) {
                Image.prototype._updateDomSrc.apply(this, arguments);
            }

            var $DomNode = this.$();
            if ($DomNode.length) {
                jQuery($DomNode).svgInject(function() {
                    // Injection complete
                });
            }
        };

        return SvgImage;
    },
    /*bExport*/ true
);

But this approach will not work with Images inside containers, ex. ObjectHeader using icon.

From my opinion, SVG would be a nice benefit for images/icons in general, because in situations where you need more than one icon color, you can use SVG and control any portion using custom css.

The above custom control works quite well, but i need to solve all kind of rerendering issues if changing src, etc. I am also using a modified version of svgInject to internally cache loaded SVGs, but this are only poor workarounds since i miss such a functionality inside core ;-(

Regards, Holger

Here is the modified svgInject

/*
    svgInject - v1.0.0
    jQuery plugin for replacing img-tags with SVG content
    by Robert Bue (@robert_bue)

    Dual licensed under MIT and GPL.
 */

;(function($, window, document, undefined) {
    var pluginName = 'svgInject';

    /**
     * Cache that helps to reduce requests
     */
    function Cache(){
        this._entries = {};
    }

    /**
     * Cache prototype extension for inheritance
     * @type {Object}
     */
    Cache.prototype = {

        /**
         * Checks if there is already an entry with that URL
         * @param  {String}  url 
         * @return {Boolean}     
         */
        isFileInCache : function( url ){
            return (typeof this._entries[url] !== 'undefined');
        },

        /**
         * Returns the entry of the given url if it exists. If not, false is returned;
         * @param  {String} url 
         * @return {Object||Boolean}     Entry or false if there is no entry with that url
         */
        getEntry : function( url ){
            return this._entries[url] || false;
        },

        /**
         * Adds an entry to the cache
         * @param {String}   url      
         * @param {Function} callback 
         */
        addEntry : function( url, callback ){

            // check if the given callback is a function
            var isCallbackFunction = (typeof callback === 'function');

            // check if the entry is already in the cache
            if(this._entries[url]){

                // if the callback is a function and the data is loaded, we execute it instantly with the cached data
                if(isCallbackFunction && !this._entries[url].isLoading){
                    callback(this._entries[url].data);
                }else if(isCallbackFunction){ // if the callback is a function and the data is still loading, we push in into the callback array
                    this._entries[url].callbacks.push(callback);
                }

                return this._entries[url];

            }else{

                var callbacks = [];

                if(isCallbackFunction){
                    callbacks.push(callback);
                }

                // put the entry into the cache 
                this._entries[url] = {
                    isLoading : true,
                    callbacks : callbacks
                };
            }
        },

        /**
         * Updates the entry after the data is loaded and executes all the callbacks for that entry
         * @param {String} url  
         * @param {*} data 
         */
        addEntryData : function( url, data ){

            var entry = this._entries[url];

            if(typeof entry !== 'undefined'){

                entry.data = data;
                entry.isLoading = false;

                this.executeEntryCallbacks(url);

            }

        },

        /**
         * Executes all callback for the entry with the given url
         * @param  {String} url 
         */
        executeEntryCallbacks : function( url ){

            var entry = this._entries[url];

            if(typeof entry !== 'undefined'){
                for(var i = 0, j = entry.callbacks.length; i < j; i++){
                    entry.callbacks.shift()(entry.data);
                    i--;
                    j--;
                }
            }

        }

    };

    function Plugin(element, options) {
        this.element = element;
        this.$element = $(element);
        this.callback = options;
        this._name = pluginName;
        this._cache = Plugin._cache;
        this.init();
    }

    Plugin._cache = new Cache();

    Plugin.prototype = {
        init: function() {
            //this.$element.css('visibility', 'hidden');
            this.injectSVG(this.$element, this.callback);
        },
        injectSVG: function( $el, callback) {
            var imgURL = $el.attr('src');
            var imgID = $el.attr('id');
            var imgClass = $el.attr('class');
            var imgStyle = $el.attr('style');
            var imgData = $el.clone(true).data();

            var dimensions = {
                w: $el.attr('width'),
                h: $el.attr('height')
            };

            var _this = this;

            // If the file is not in the cache, we request it 
            if(!this._cache.isFileInCache(imgURL)){
                $.get(imgURL, function(data) {
                    var svg = $(data).find('svg');
                    // File is put into the cache
                    _this._cache.addEntryData( imgURL, svg );
                });  
            }

            // We add an entry to the cache with the given image url
            this._cache.addEntry(imgURL , function( svg ){
                // When the entry is loaded, the image gets replaces by the clone of the loaded svg
                _this.replaceIMGWithSVG($el, svg.clone(), imgID, imgClass, imgStyle, imgURL, imgData, dimensions, callback);
            });
        },
        replaceIMGWithSVG : function( $el, svg, imgID, imgClass, imgStyle, imgURL, imgData, dimensions, callback ){

            if (typeof imgID !== undefined) {
                svg = svg.attr('id', imgID);
            }

            if (typeof imgClass !== undefined) {
                var cls = (svg.attr('class') !== undefined) ? svg.attr('class') : '';
                svg = svg.attr('class', imgClass + ' ' + cls + ' replaced-svg');
            }

            if (typeof imgStyle !== undefined) {
                svg = svg.attr('style', imgStyle);
            }

            if (typeof imgURL !== undefined) {
                svg = svg.attr('data-url', imgURL);
            }

            $.each(imgData, function(name, value) {
                svg[0].setAttribute('data-' + name, value);
            });

            svg = svg.removeAttr('xmlns:a');

            var ow = parseFloat(svg.attr('width'));
            var oh = parseFloat(svg.attr('height'));

            if (dimensions.w && dimensions.h) {
                $(svg).attr('width', dimensions.w);
                $(svg).attr('height', dimensions.h);
            } else if (dimensions.w) {
                $(svg).attr('width', dimensions.w);
                $(svg).attr('height', (oh / ow) * dimensions.w);
            } else if (dimensions.h) {
                $(svg).attr('height', dimensions.h);
                $(svg).attr('width', (ow / oh) * dimensions.h);
            }

            $el.replaceWith(svg);

            var js = new Function(svg.find('script').text());

            js();

            if ( typeof callback === 'function' ) {
                callback();
            }
        }
    };

    $.fn[pluginName] = function(options) {
        return this.each(function() {
            if (!$.data(this, 'plugin_' + pluginName)) {
                $.data(this, 'plugin_' + pluginName, new Plugin(this, options));
            }
        });
    };
})(jQuery, window, document);

hschaefer123 avatar Dec 21 '15 15:12 hschaefer123

Thanks! I've forwarded this feature request to the responsible product owner.

matz3 avatar Dec 22 '15 09:12 matz3

Hi @matz3 what's the status on this task ?.

Thanks, Ted.

tdniksfsap1 avatar Feb 02 '17 09:02 tdniksfsap1

In the meantime, i have created a custom control and also a set of prototypes to support this in our tools!

See my blog http://openui5.blogspot.com/2017/01/svgimage.html

I think, the whole UI5 lib would benefit from SVG support (and i would be more stable accross releases ;-)

Inside my blog Demo, i had to fix some CSS classes to use images instead icons accross the widgets. Mostly padding/margin issues, because no one seems to test the usage of images instead of icons at the possible places ;-)

Widthout the fixes, the UI looks a little bit ugly.

/* sap.m.Button css fix */
.sapMBtnIcon.uiSVGImage {
	padding: 0.25rem;
    box-sizing: border-box;
	height: 100%;
}

/* sap.m.MessageStrip custom icon fix */
.sapMMsgStripIcon>svg {
	width: auto;
	height: 1rem;
}

/* sap.ui.unified fix */
.sapUiMnuItmIco>svg {
	padding-left: 0;
	max-width: 1rem;
	max-height: 1rem;
}

The fixes are also needed for the usage of regular images instead of icons!

Regards Holger

hschaefer123 avatar Feb 02 '17 09:02 hschaefer123

@tdniksfsap1 No update, yet. I've opened an internal ticket (1780100846) to clarify if this is something we would see as a new feature for sap.m.Image or not.

matz3 avatar Feb 07 '17 08:02 matz3

Hi Holger, this is very interesting functionality about the Image and changing themes. We haven't had such a requirement till now, but I would take the proposal to our PO and Architect for discussion and implementing a possible feature.

Great Thanks, Mihail.

myordanov avatar Feb 08 '17 11:02 myordanov

I need this aswell. Especially because I want to access the path of an svg images. I need the image rendered as an <object> because I have to access contentDocument.

ManuelB avatar Feb 22 '17 14:02 ManuelB

This enhancement will be covered in BLI FIORITECHE1-1513. Any progress will be posted here.

flovogt avatar Mar 18 '21 08:03 flovogt

Hello, The request has been prioritized to be addressed in one of the coming two takts. I will add updates here. Best regards, Jordan

jdichev avatar Apr 08 '22 10:04 jdichev

Hello @hschaefer123,

A bit of clarification needed.

Do I understand you correctly that the current possibility of displaying an SVG file inside image by setting a path to the src property of Image control is not entirely enough?

If yes, do you mean that the Image control needs to make it easier to reference SVG files via the src property but instead of passing this internally to an IMG tag, rather fetch and display the SVG inline so you can access the DOM and manipulate it with CSS?

In the light of this, @ManuelB can you provide a bit more information on your use case?

Thanks and regards, Jordan

jdichev avatar Apr 27 '22 10:04 jdichev

@jdichev we used an svg map from germany with all federal states. They were clickable.

ManuelB avatar Apr 27 '22 12:04 ManuelB

Hi @jdichev, you are right! We need exactly the svg inlined like other tools like vue-inline-svg doing also.

The main reason is to be able to apply the current theme colors to svg images. For example, an avatar svg, that can have multiple color layers, that can be themed with plain style like

#svgIdLayer1 { fillcolor: var (--sapBrandColor); }

Like in the concept off IllustrationMessages, you would be able to use SVG images that adapt custom theming on-the-fly.

We also do things like @ManuelB to be able to use/generate ExplorationImageSVG files to be able to generate technical pictures with spareparts that have clickable regions to graphically drill down into BOMs.

Best regards Holger

hschaefer123 avatar Apr 27 '22 13:04 hschaefer123

Hello @hschaefer123,

Thanks for the clarification. Additionally, I need to ask how do you intend to manage the CSS for such cases as this is something that should hand in hand - the SVG and the respective CSS?

@IlianaB FYI

Best regards, Jordan

jdichev avatar Apr 28 '22 10:04 jdichev

ping @hschaefer123 for clarification

flovogt avatar Jul 25 '22 15:07 flovogt

Hi, since we do not have controls that include css, generally the svg will contain custom css classes or ids.

For that reason, i will use a custom.css file added to the manifest.

With vue for example, this is easier with scoped css, but this is out-of-scope for ui5.

Best regards Holger

hschaefer123 avatar Jul 25 '22 16:07 hschaefer123

This issue was covered in backlog BGSOFUIPIRIN-5679 and implemented with https://github.com/SAP/openui5/commit/d459de2f3a44e76d7f608cf58b10cfd646185c91.

flovogt avatar Aug 25 '22 13:08 flovogt