XF 2.0 Extending JS function

[F.A] Walky

Active member
Hello,

I'd like to extend the XF.InlineMod.selectAll JS function in inline_mod.js at line 114 !
So I created a little JS file with this :
Code:
!function($, window, document, _undefined)
{
    "use strict";

    XF.Element.extend('inline-mod', {

        selectAll:function()
        {
            return XF.InlineMod.prototype.selectAll();
        }
    });
}
(jQuery, window, document);

But I got an error in my browser console, which says: Uncaught TypeError: Cannot read property 'find' of null, so it seems like the this parameter isn't sent again when extending, or maybe I'm missing something ? :rolleyes:

Thank you :)
 
This may help:

Improved JS Framework
XF2 itself still uses jQuery as its primary JS framework but has removed its dependency on the aging and unsupported jQuery Tools. We have built our own framework around jQuery which makes instantiation and extension of JS easier.

We have two types of handler; a click handler and an element handler. Each handler type is an object which has similar behaviors and similar functions within that should be extended. Click handlers execute code once the element is clicked, and element handlers execute once the element is initialized. Let's take a look at a basic click handler:

JavaScript:
XF.LinkClick = XF.Click.newHandler({
eventNameSpace: 'XFMyLinkClick',

options: {
alertText: 'Link has been clicked!'
},

init: function()
{
alert('Initialization. This fires on the first click.');
},

click: function(e)
{
e.preventDefault();

alert(this.options.alertText); // Alerts: 'Link has been clicked!'
}
});

XF.Click.register('mylink', 'XF.LinkClick');
To set up an element to call the above code when clicked, you simply add a data-xf-click attribute with a value of mylink (the identifier, the first argument of the register line).

HTML:
<a href="javascript:" data-xf-click="mylink">Click here!</a>
Some interesting things to note:
  • We specify a unique event namespace which means that a number of events will be fired when the link is clicked. Specifically, they are before the handler is initialized, after the handler is initialized, before the click code executes and after the click code executes.
  • We have an options object. Options are passed to the handler automatically and allow an element to adjust its configuration based on data-X attributes passed in. For example, when the above link is clicked, the alert "Link has been clicked!" will display. If we wanted this to alert something different, we can do that by adjusting the HTML as follows:
HTML:
<a href="javascript:" data-xf-click="mylink" data-alert-text="Something different!">Click here!</a>
Element handlers work in a nearly identical way, but the init function for element handlers executes when the element is activated (usually on page load for the entire document, or on specific elements on demand).
It is also trivially possible to extend an existing handler. Let's take a look at a very basic element handler which when assigned to an element will change its text color to red, and underlined:

JavaScript:
XF.RedText = XF.Element.newHandler({
options: {
color: 'red',
decoration: 'underline'
},

init: function()
{
this.changeColor();
this.underline();
this.alert();
},

changeColor: function()
{
this.$target.css({
color: this.options.color
});
},

underline: function()
{
this.$target.css({
'text-decoration': this.options.decoration
});
},

alert: function()
{
alert('The text changed to red and underline...');
}
});

XF.Element.register('red-text', 'XF.RedText');
HTML:
<div data-xf-init="red-text">This is black text.</div>
When this element initializes, the "This is black text" text will turn red and become underlined and an alert 'The text changed to red and underline...' will be displayed on the screen.

We might want to re-use or extend this code for a different element handler, and this is possible using XF.extend. This allows you to extend an existing handler with your own code. You can then change default options, or even override or extend entire functions with the added option of still calling the original function if desired. Let's see that in action:

JavaScript:
XF.BlueText = XF.extend(XF.RedText, {
__backup: {
'alert': '_alert'
},

options: $.extend({}, XF.RedText.prototype.options, {
color: 'blue',
decoration: 'line-through'
}),

alert: function()
{
this._alert();
alert('...actually, it changed to blue and strike through!');
}
});

XF.Element.register('blue-text', 'XF.BlueText');
HTML:
<div data-xf-init="blue-text">This is black text.</div>
What we have done here is we have registered a new element handler called blue-text but the bulk of the behavior is coming from the red-text handler.

Notice that the default options of the red-text handler are overridden so that the text is going to be blue and line-through rather than red and underline.

We've also used a __backup object to map the alert function in the original handler to a new name of _alert in the new handler. This is equivalent to extending a class and method in PHP and calling the parent:: method.

The end result of this blue-text handler is that the color of the text is changed to blue and underline (based on our extended options), the alert text from the original handler is displayed ('The text changed to red and underline...'), followed by the new alert text we've specified in the new handler ('...actually, it changed to blue and strike through!').

While this demonstrates a clear hierarchy style, we have also exposed a more dynamic method of extension. This would likely fall into the realm of "monkey patching".

JavaScript:
XF.Element.extend('red-text', {
alert: function()
{
alert('Hey a different message');
}
});
This dynamically extends the XF.RedText instance in place and overrides the functionality of the alert function. All code that is using the red-text element will automatically use the extended version. Further, multiple extensions can happen to the same element handler without causing name conflicts; in this regard, it's similar to the class extension system for PHP.

Additionally, this XF.Element.extend method can be called before or after the base element is registered. This is relevant when JS is loaded asynchronously. (Clearly, it needs to be called before the element is instantiated, but the JS framework won't do that until all pending asynchronous JS has loaded.)
 
If I understand correctly I can add JavaScript code to extend the default JavaScript, is this correct?

So for example I would like to change the following which is located in the attachment_manager.js file:

JavaScript:
		insertAttachment: function($row, action)
		{
			var attachmentId = $row.data('attachment-id');
			if (!attachmentId)
			{
				return;
			}
			if (!this.editor)
			{
				return;
			}

			var thumb = $row.find(this.options.templateThumb).attr('src'),
				view = $row.find(this.options.templateView).attr('href');

			var html, bbCode, params = {
				id: attachmentId,
				img: thumb
			};

			if (action == 'full')
			{
				bbCode = '[ATTACH=full]' + attachmentId + '[/ATTACH]';
				html = '<img src="{{img}}" data-attachment="full:{{id}}" alt="{{id}}" />';

				params.img = view;
			}
			else
			{
				if (!thumb)
				{
					return;
				}

				bbCode = '[ATTACH]' + attachmentId + '[/ATTACH]';
				html = '<img src="{{img}}" data-attachment="thumb:{{id}}" alt="{{id}}" />';
			}

			html = Mustache.render(html, params);
			XF.insertIntoEditor(this.$target, html, bbCode, '[data-attachment-target=false]');
		},

How would I extend this JavaScript code?
 
Last edited:
Well the bit I quoted above pretty much covers all options available in terms of extending JS code.

If you want anything more specific than that, you'll have to tell us what you want to change in the code specifically so we can advise the best way to perform the extension.
 
I'm trying to extend xf/js/profile_banner.js

I've edited the original file and it works, so trying to extend it now.

I've edited the ajaxResponse function and am trying to replace the whole thing

ajaxResponse: function (e, data) { ...}

I've tried various forms of the following to no avail

JavaScript:
!function ($, window, document, _undefined) {
    "use strict";

    XF.extend(XF.BannerUpload, {

        // __backup: {
        //     'ajaxResponse': '_ajaxResponse'
        // },

        ajaxResponse: function (e, data) {
            ...
        }
    })
}
            
(jQuery, window, document);

This is the original file

Code:
!function($, window, document, _undefined)
{
    "use strict";

    // ################################## PROFILE BANNER UPLOAD HANDLER ###########################################

    XF.BannerUpload = XF.Element.newHandler({

        options: {},

        init: function()
        {
            var $form = this.$target,
                $file = $form.find('.js-uploadBanner'),
                $banner = $form.find('.js-banner'),
                $container = $banner.closest('.profileBannerContainer'),
                $deleteButton = $form.find('.js-deleteBanner');

            if ($container.hasClass('profileBannerContainer--withBanner'))
            {
                $deleteButton.show();
            }
            else
            {
                $deleteButton.hide();
            }

            $file.on('change', XF.proxy(this, 'changeFile'));
            $form.on('ajax-submit:response', XF.proxy(this, 'ajaxResponse'));
        },

        changeFile: function(e)
        {
            if ($(e.target).val() != '')
            {
                this.$target.submit();
            }
        },

        ajaxResponse: function(e, data)
        {
            if (data.errors || data.exception)
            {
                return;
            }

            e.preventDefault();

            if (data.message)
            {
                XF.flashMessage(data.message, 3000);
            }

            var $form = this.$target,
                $delete = $form.find('.js-deleteBanner'),
                $file = $form.find('.js-uploadBanner'),
                $banner = $form.find('.js-banner'),
                $container = $banner.closest('.profileBannerContainer'),
                banners = data.banners,
                position = data.position,
                bannerCount = Object.keys(banners).length,
                classPrefix = 'memberProfileBanner-u' + data.userId + '-';

            $file.val('');

            $('.memberProfileBanner').each(function()
            {
                var $thisBanner = $(this),
                    // $thisParent = $thisBanner.parent().parent(),  // Alex - add .parent() to fix deleting banenr
                    $thisParent = $thisBanner.parent(),  // Alex - add .parent() to fix deleting banenr
                    hideEmpty = $thisBanner.data('hide-empty'),
                    toggleClass = $thisBanner.data('toggle-class'),
                    newBanner;

                if ($thisBanner.is('[class*="' + classPrefix + '"]'))
                {
                    if ($thisBanner.hasClass(classPrefix + 'm'))
                    {
                        newBanner = banners['m'];
                    }
                    else if ($thisBanner.hasClass(classPrefix + 'l'))
                    {
                        newBanner = banners['l'];
                    }
                    else if ($thisBanner.hasClass(classPrefix + 'o'))
                    {
                        newBanner = banners['o'];
                    }
                }

                // Alex cover photo
                //$thisBanner.parent().addClass("bbImageWrapper", newBanner ? 'url(' + newBanner + ')' : 'none');
                //$thisBanner.parent().attr("data-src", newBanner ? newBanner : 'none');
                //$thisBanner.attr("data-src", newBanner ? newBanner : 'none');

                $thisBanner.css({
                    'background-image': newBanner ? 'url(' + newBanner + ')' : 'none',
                    'background-position-y': position !== null ? position + '%' : null
                });

                if (hideEmpty)
                {
                    if (!newBanner)
                    {
                        $thisBanner.addClass('memberProfileBanner--empty');
                    }
                    else
                    {
                        $thisBanner.removeClass('memberProfileBanner--empty');
                    }
                }

                $thisBanner.trigger('profile-banner:refresh');

                if (toggleClass)
                {
                    if (!newBanner)
                    {
                        $thisParent.removeClass(toggleClass);
                    }
                    else
                    {
                        $thisParent.addClass(toggleClass);
                    }
                }
            });

            if (!bannerCount)
            {
                $delete.hide();
                $container.removeClass('profileBannerContainer--withBanner');
            }
            else
            {
                $delete.show();
                $container.addClass('profileBannerContainer--withBanner');
            }
        }
    });

    // ################################## BANNER POSITIONER HANDLER ###########################################

    XF.BannerPositioner = XF.Element.newHandler({

        options: {},

        $banner: null,
        $value: null,
        y: 0,

        ns: 'bannerPositioner',
        dragging: false,
        scaleFactor: 1,

        init: function()
        {
            var $banner = this.$target;

            this.$banner = $banner;
            $banner.css({
                'touch-action': 'none',
                'cursor': 'move'
            });

            this.$value = $banner.find('.js-bannerPosY');

            this.initDragging();

            var t = this;

            $banner.on('profile-banner:refresh', function()
            {
                var yPos = $banner.css('background-position-y');
                if (yPos)
                {
                    t.$value.val(parseFloat(yPos));
                }

                t.stopDragging();
                $banner.off('.' + t.ns);
                t.initDragging();
            });
        },

        initDragging: function()
        {
            var ns = this.ns,
                $banner = this.$banner,
                imageUrl = $banner.css('background-image'),
                image = new Image(),
                t = this;

            imageUrl = imageUrl.replace(/^url\(["']?(.*?)["']?\)$/i, '$1');
            if (!imageUrl)
            {
                return;
            }

            image.onload = function()
            {
                var setup = function()
                {
                    // scaling makes pixel-based pointer movements map to percentage shifts
                    var displayScale = image.width ? $banner.width() / image.width : 1;
                    t.scaleFactor = 1 / (image.height * displayScale / 100);

                    $banner.on('mousedown.' + ns + ' touchstart.' + ns, XF.proxy(t, 'dragStart'));
                };

                if ($banner.width() > 0)
                {
                    setup();
                }
                else
                {
                    // it's possible for this to be triggered when the banner container has been hidden,
                    // so only allow this to be triggered again once we know the banner is visible
                    $banner.one('mouseover.' + ns + ' touchstart.' + ns, setup);
                }
            };
            image.src = XF.canonicalizeUrl(imageUrl);
        },

        dragStart: function(e)
        {
            e.preventDefault();

            var oe = e.originalEvent,
                ns = this.ns;

            if (oe.touches)
            {
                this.y = oe.touches[0].clientY;
            }
            else
            {
                this.y = oe.clientY;

                if (oe.button > 0)
                {
                    // probably a right click or similar
                    return;
                }
            }

            this.dragging = true;

            $(window)
                .on('mousemove.' + ns + ' touchmove.' + ns, XF.proxy(this, 'dragMove'))
                .on('mouseup.' + ns + ' touchend.' + ns, XF.proxy(this, 'dragEnd'));
        },

        dragMove: function(e)
        {
            if (this.dragging)
            {
                e.preventDefault();

                var oe = e.originalEvent,
                    existingPos = parseFloat(this.$banner.css('background-position-y')),
                    newY, newPos;

                if (oe.touches)
                {
                    newY = oe.touches[0].clientY;
                }
                else
                {
                    newY = oe.clientY;
                }

                newPos = existingPos + (this.y - newY) * this.scaleFactor;
                newPos = Math.min(Math.max(0, newPos), 100);

                this.$banner.css('background-position-y', newPos + '%');
                this.$value.val(newPos);
                this.y = newY;
            }
        },

        dragEnd: function(e)
        {
            this.stopDragging();
        },

        stopDragging: function()
        {
            if (this.dragging)
            {
                $(window).off('.' + this.ns);

                this.y = 0;
                this.dragging = false;
            }
        }
    });

    XF.Element.register('banner-upload', 'XF.BannerUpload');
    XF.Element.register('banner-positioner', 'XF.BannerPositioner');
}
(jQuery, window, document);

To test it, I'm including it in the account_banner template just below the original profile_banner.js includes <xf:js src="xf/profile_banner.js" /> in some <xf:js> tags.

Any idea what I'm doing wrong?
 
You'd need to re-register it with extended handler:

JavaScript:
!function ($, window, document, _undefined)
{
    "use strict";

    XF.NewBannerUpload = XF.extend(XF.BannerUpload,
    {
        __backup: {
            'ajaxResponse': '_ajaxResponse'
        },

        ajaxResponse: function (e, data) {
            // your code here
        }
    });

    XF.Element.register('banner-upload', 'XF.NewBannerUpload');
}
(jQuery, window, document);

Or alternatively use the monkey-patch approach:

JavaScript:
XF.Element.extend('banner-upload', 
{
    ajaxResponse: function (e, data) {
        // your code here
    }
});
 
Last edited:
Top Bottom