7

I am using 6.0 Sometime I am getting weird problem in ExtJS 6 tagfield. I am using a tagfield with growMax : 3. Now When value selected for tagfield is more than three tyhen I am getting a pointer up and down option in tagfield.

This is fine Now the weired part is when I am click on down arrow this taking me exactly the bottom of field. I can not see what else value is selected which is placed in between. Is there any way I can slow the speed of moving scroll of those value.

My Fiddle : Fiddle

Step to reproduce.

  1. Select few values(more than 2 or 3)
  2. Click on down pointer. (Red box in image) Image

It will may skip 2nd third value and leads you to end.

Note : Sometime I need to perform this in for 100 data in tagfield. Can't even see what and all I selected. Also I can't change height.

Is there any event which fier on click of scroll buttons.

David
  • 4,266
  • 8
  • 34
  • 69
  • Is it worth considering that you could remove `filterPickList: true`? Or perhaps opting for a combobox with multiple selection instead? You mention you may need to view up to 100 tags in the field, but viewing them one or two at a time by scrolling slowly doesn't seem practical. – Jaimee Mar 22 '17 at 18:55
  • Yes, I can remove `filterPickList: true` and test. combobox with multiple selection I can not use because my app is extJs 6. – David Mar 23 '17 at 04:59
  • @Jaimee No even after `filterPickList: true` things are same. – David Mar 23 '17 at 07:57
  • May I know what is the reason for downvote ? – David Mar 24 '17 at 09:37
  • I wasn't suggesting it as a fix, but as an alternate option for users to view what they have selected. To scroll through 100 tags this way would take a long time. What if an end user had to look for and deselect the 70th item? Maybe a UI review is needed - It's a lot of information to show in a small space. – Jaimee Mar 24 '17 at 16:07
  • @David what edition of IE are you working with? – blackmiaool Apr 01 '17 at 10:25
  • @blackmiaool, according to comments under my answer IE 11 is used but I'l still not sure how this new IE-specific bad behavior looks like – SergGr Apr 01 '17 at 22:32
  • @SergGr I am using your code in fiddler. https://fiddle.sencha.com/#view/editor&fiddle/1t9r Now after selection click on the down arrow. You will get difference in Chrome and IE – David Apr 03 '17 at 06:00
  • @David, is it true that this fiddle does not use my latest inheritance-based approach and thus `onItemListClick` doesn't override a [built-in method of `Tag` field](https://docs.sencha.com/extjs/6.0.2/classic/Ext.form.field.Tag.html#method-onItemListClick)? Can you reproduce the issue with my actual last code with IE hack? – SergGr Apr 03 '17 at 13:07

2 Answers2

4

Update (actually implement inheritance)

OK, it looks like you really need an inheritance-based solution. This code is obviously not an idiomatic ExtJS but it seems to work for me. First define a custom subclass SingleLineTag and assign 'singleline-tagfield' as its xtype (there is some description of the main ideas behind this code in the "older answer" section)

Ext.define('Ext.form.field.SingleLineTag', {
    extend: 'Ext.form.field.Tag',
    xtype: 'singleline-tagfield',

    initEvents: function () {
        var me = this;
        me.callParent(arguments);

        me.itemList.el.dom.parentElement.addEventListener('scroll', Ext.bind(me.zzzOnTagScroll, me));
    },

    zzzGetTagLastScroll: function () {
        var me = this;
        return me.zzzLastScroll = me.zzzLastScroll || {
                lastIndex: 0,
                lastTop: 0,
                lastTimeStamp: 0
            };
    },

    zzzScrollToTagIndex: function (index) {
        var tagField = this;
        var lastScroll = tagField.zzzLastScroll;
        if (lastScroll) {
            var lstDom = tagField.itemList.el.dom;
            var childrenDom = lstDom.children;
            var containerDom = tagField.itemList.el.dom.parentElement;

            if ((index >= 0) && (index < childrenDom.length)) {
                lastScroll.lastIndex = index;
                containerDom.scrollTop = lastScroll.lastTop = childrenDom[index].offsetTop - lstDom.offsetTop;
            }
        }
    },

    zzzOnTagScroll: function (ev) {
        var me = this;
        var lastScroll = me.zzzGetTagLastScroll();

        // throttle scroll events as thy occur to often and we might scroll to much
        if (Math.abs(lastScroll.lastTimeStamp - ev.timeStamp) < 200) {
            ev.preventDefault();
            return;
        }

        lastScroll.lastTimeStamp = ev.timeStamp;

        var lstDom = me.itemList.el.dom;
        var childrenDom = lstDom.children;
        var containerDom = me.itemList.el.dom.parentElement;
        var scrollTop = containerDom.scrollTop;

        var index = lastScroll.lastIndex;
        if (index >= childrenDom.length)
            index = childrenDom.length - 1;
        if (index < 0)
            index = 0;
        var lstTop = lstDom.offsetTop;
        if (scrollTop > lastScroll.lastTop) {
            // scrolling down, find next element
            for (; index < childrenDom.length; index++) {
                if (childrenDom[index].offsetTop - lstTop > scrollTop) {
                    break;
                }
            }
            if (index < childrenDom.length) {
                // we've found the next element so change scroll position to it's top
                me.zzzScrollToTagIndex(index);
            }
            else {
                lastScroll.lastIndex = childrenDom.length;
                lastScroll.lastTop = containerDom.scrollTop;
            }
        }
        else {
            // scrolling up, find prev element
            for (; index >= 0; index--) {
                if (childrenDom[index].offsetTop - lstTop < scrollTop) {
                    break;
                }
            }
            if (index >= 0) {
                // we've found the prev element so change scroll position to it's top
                me.zzzScrollToTagIndex(index);
            }
            else {
                lastScroll.lastIndex = 0;
                lastScroll.lastTop = 0;
            }
        }
    },


    onBeforeDeselect: function (list, record) {
        var me = this;
        me.callParent(arguments);
        var value = record.get(me.valueField);
        var index = me.getValue().indexOf(value);
        var lastScroll = me.zzzGetTagLastScroll();
        if (lastScroll.lastIndex > index)
            lastScroll.lastIndex -= 1;
        var nextIndex = (lastScroll.lastIndex > index) ? lastScroll.lastIndex - 1 : lastScroll.lastIndex;
        setTimeout(function () {
            me.zzzScrollToTagIndex(nextIndex);
        }, 0);
    },

    onItemListClick: function(ev) {
        var me = this;

        // HACK for IE: throttle click events after scroll
        // click on the scrollbar seem to generate click on the "itemList" as well
        // which lead to showing of the dropdown
        var lastScroll = me.zzzGetTagLastScroll();
        if (Math.abs(lastScroll.lastTimeStamp - ev.timeStamp) > 200) {
            me.callParent(arguments);
        }
    }
});

Now change the xtype in the items collections

var shows = Ext.create('Ext.data.Store', {
    fields: ['id', 'show'],
    data: [
        {id: 0, show: 'Battlestar Galactica'},
        {id: 11, show: 'Doctor Who'},
        {id: 2, show: 'Farscape'},
        {id: 3, show: 'Firefly'},
        {id: 4, show: 'Star Trek'},
        {id: 5, show: 'Star Wars: Christmas Special'}
    ]
});

Ext.create('Ext.form.Panel', {
    renderTo: Ext.getBody(),
    title: 'Sci-Fi Television',
    height: 200,
    width: 300,
    items: [{
        //xtype: 'tagfield',          // old
        xtype: 'singleline-tagfield', // new 
        growMax: 18,
        fieldLabel: 'Select a Show',
        store: shows,
        displayField: 'show',
        valueField: 'id',
        queryMode: 'local',
        filterPickList: true,
    }]
}); 

Note that if you don't configure this element to actually take only single line of height, it will behave weirdly in terms of scrolling.

See combined code at this Sencha fiddle


Older answer

I'm not good with ExtJS but I think some not so good answer is better than no answer at all. First of all I agree that growMax is in pixels and thus 3 is too little. Still considering your issue, it seems that there is just not enough space for a full-blown scrollbar and thus the only way is to add custom scrolling logic. Probably it is better to create some new class that inherits from Tag but I'm not sure how exactly to do it properly in the ExtJS so here is some custom and probably non-idiomatic code.

function findComponentByElement(node) {
    var topmost = document.body,
        target = node,
        cmp;

    while (target && target.nodeType === 1 && target !== topmost) {
        cmp = Ext.getCmp(target.id);

        if (cmp) {
            return cmp;
        }

        target = target.parentNode;
    }

    return null;
}


var getTagLastScroll = function (tagField) {
    return tagField.zzzLastScroll = tagField.zzzLastScroll || {
            lastIndex: 0,
            lastTop: 0,
            lastTimeStamp: 0
        };
};

var scrollToTagIndex = function (tagField, index) {
    var lastScroll = tagField.zzzLastScroll;
    if (lastScroll) {
        var lstDom = tagField.itemList.el.dom;
        var childrenDom = lstDom.children;
        var containerDom = tagField.itemList.el.dom.parentElement;

        if ((index >= 0) && (index < childrenDom.length)) {
            lastScroll.lastIndex = index;
            containerDom.scrollTop = lastScroll.lastTop = childrenDom[index].offsetTop - lstDom.offsetTop;

            //console.log("Scroll to " + containerDom.scrollTop);
            //console.log(lastScroll);

        }
    }
};

var onTagScroll = function (ev) {
    var tagField = findComponentByElement(ev.target);
    var lastScroll = getTagLastScroll(tagField);

    // need to throttle scroll events or will scroll to much
    if (Math.abs(lastScroll.lastTimeStamp - ev.timeStamp) < 200) {
        ev.preventDefault();
        return;
    }

    //console.log(ev);
    lastScroll.lastTimeStamp = ev.timeStamp;

    var lstDom = tagField.itemList.el.dom;
    var childrenDom = lstDom.children;
    var containerDom = tagField.itemList.el.dom.parentElement;
    var scrollTop = containerDom.scrollTop;

    //console.log("Before " + containerDom.scrollTop);
    //console.log(lastScroll);

    var index = lastScroll.lastIndex;
    if (index >= childrenDom.length)
        index = childrenDom.length - 1;
    if (index < 0)
        index = 0;
    var lstTop = lstDom.offsetTop;
    if (scrollTop > lastScroll.lastTop) {
        // scrolling down, find next element
        for (; index < childrenDom.length; index++) {
            if (childrenDom[index].offsetTop - lstTop > scrollTop) {
                break;
            }
        }
        if (index < childrenDom.length) {
            // we've found the next element so change scroll position to it's top
            scrollToTagIndex(tagField, index);
        }
        else {
            lastScroll.lastIndex = childrenDom.length;
            lastScroll.lastTop = containerDom.scrollTop;
        }
    }
    else {
        // scrolling up, find prev element
        for (; index >= 0; index--) {
            if (childrenDom[index].offsetTop - lstTop < scrollTop) {
                break;
            }
        }
        if (index >= 0) {
            // we've found the prev element so change scroll position to it's top
            scrollToTagIndex(tagField, index);
        }
        else {
            lastScroll.lastIndex = 0;
            lastScroll.lastTop = 0;
        }
    }
    //console.log("After " + containerDom.scrollTop);
    //console.log(lastScroll);
};


var beforeDeselect = function (tagField, record) {
    var value = record.get(tagField.valueField);
    var index = tagField.getValue().indexOf(value);
    var lastScroll = getTagLastScroll(tagField);
    if (lastScroll.lastIndex > index)
        lastScroll.lastIndex -= 1;
    var nextIndex = (lastScroll.lastIndex > index) ? lastScroll.lastIndex - 1 : lastScroll.lastIndex;
    setTimeout(function () {
        scrollToTagIndex(tagField, nextIndex);
    }, 0);
};

var attachCustomScroll = function (tagField) {
    var containerDom = tagField.itemList.el.dom.parentElement;
    containerDom.addEventListener('scroll', onTagScroll);
    tagField.on('beforeDeselect', beforeDeselect);
};

You can use it by simply doing something like

var pnl = Ext.create('Ext.form.Panel', { 
    ...
});
var tagField = pnl.items.items[0];
attachCustomScroll(tagField);

The main idea behing my code is to intercept scroll event for the container element that contains ul with selected items and treat events not as real scrolling but just as a direction to scroll by one element to it. Data that is needed for that to work correctly is attached back to the widget under hopefully unique zzzLastScroll name.

Also there is additional piece of logic to make scrolling look better when some item is removed.


Full code (instead of fiddle)

Unfortunatelly I don't have ExtJS account and without it I can't create a new fiddle there so just in case here is full code of modified app.js that I used to test my code.

var shows = Ext.create('Ext.data.Store', {
    fields: ['id', 'show'],
    data: [
        {id: 0, show: 'Battlestar Galactica'},
        {id: 11, show: 'Doctor Who'},
        {id: 2, show: 'Farscape'},
        {id: 3, show: 'Firefly'},
        {id: 4, show: 'Star Trek'},
        {id: 5, show: 'Star Wars: Christmas Special'}
    ]
});


var pnl = Ext.create('Ext.form.Panel', {
    renderTo: Ext.getBody(),
    title: 'Sci-Fi Television',
    height: 200,
    width: 300,
    items: [{
        xtype: 'tagfield',
        growMax: 18,
        fieldLabel: 'Select a Show',
        store: shows,
        displayField: 'show',
        valueField: 'id',
        queryMode: 'local',
        filterPickList: true,


    }]
});

window.tagField = pnl.items.items[0];
window.lstDom = window.tagField.itemList.el.dom;
window.container = window.lstDom.parentElement;

tagField.setValue([11, 3, 4, 5]);

function findComponentByElement(node) {
    var topmost = document.body,
        target = node,
        cmp;

    while (target && target.nodeType === 1 && target !== topmost) {
        cmp = Ext.getCmp(target.id);

        if (cmp) {
            return cmp;
        }

        target = target.parentNode;
    }

    return null;
}


var getTagLastScroll = function (tagField) {
    return tagField.zzzLastScroll = tagField.zzzLastScroll || {
            lastIndex: 0,
            lastTop: 0,
            lastTimeStamp: 0
        };
};

var scrollToTagIndex = function (tagField, index) {
    var lastScroll = tagField.zzzLastScroll;
    if (lastScroll) {
        var lstDom = tagField.itemList.el.dom;
        var childrenDom = lstDom.children;
        var containerDom = tagField.itemList.el.dom.parentElement;

        if ((index >= 0) && (index < childrenDom.length)) {
            lastScroll.lastIndex = index;
            containerDom.scrollTop = lastScroll.lastTop = childrenDom[index].offsetTop - lstDom.offsetTop;

            //console.log("Scroll to " + containerDom.scrollTop);
            //console.log(lastScroll);

        }
    }
};

var onTagScroll = function (ev) {
    var tagField = findComponentByElement(ev.target);
    var lastScroll = getTagLastScroll(tagField);

    if (Math.abs(lastScroll.lastTimeStamp - ev.timeStamp) < 200) {
        ev.preventDefault();
        return;
    }

    //console.log(ev);
    lastScroll.lastTimeStamp = ev.timeStamp;

    var lstDom = tagField.itemList.el.dom;
    var childrenDom = lstDom.children;
    var containerDom = tagField.itemList.el.dom.parentElement;
    var scrollTop = containerDom.scrollTop;

    //console.log("Before " + containerDom.scrollTop);
    //console.log(lastScroll);

    var index = lastScroll.lastIndex;
    if (index >= childrenDom.length)
        index = childrenDom.length - 1;
    if (index < 0)
        index = 0;
    var lstTop = lstDom.offsetTop;
    if (scrollTop > lastScroll.lastTop) {
        // scrolling down, find next element
        for (; index < childrenDom.length; index++) {
            if (childrenDom[index].offsetTop - lstTop > scrollTop) {
                break;
            }
        }
        if (index < childrenDom.length) {
            // we've found the next element so change scroll position to it's top
            scrollToTagIndex(tagField, index);
        }
        else {
            lastScroll.lastIndex = childrenDom.length;
            lastScroll.lastTop = containerDom.scrollTop;
        }
    }
    else {
        // scrolling up, find prev element
        for (; index >= 0; index--) {
            if (childrenDom[index].offsetTop - lstTop < scrollTop) {
                break;
            }
        }
        if (index >= 0) {
            // we've found the prev element so change scroll position to it's top
            scrollToTagIndex(tagField, index);
        }
        else {
            lastScroll.lastIndex = 0;
            lastScroll.lastTop = 0;
        }
    }
    //console.log("After " + containerDom.scrollTop);
    //console.log(lastScroll);
};


var beforeDeselect = function (tagField, record) {
    var value = record.get(tagField.valueField);
    var index = tagField.getValue().indexOf(value);
    var lastScroll = getTagLastScroll(tagField);
    if (lastScroll.lastIndex > index)
        lastScroll.lastIndex -= 1;
    var nextIndex = (lastScroll.lastIndex > index) ? lastScroll.lastIndex - 1 : lastScroll.lastIndex;
    setTimeout(function () {
        scrollToTagIndex(tagField, nextIndex);
    }, 0);
};

var attachCustomScroll = function (tagField) {
    var containerDom = tagField.itemList.el.dom.parentElement;
    containerDom.addEventListener('scroll', onTagScroll);
    tagField.on('beforeDeselect', beforeDeselect);
};

attachCustomScroll(window.tagField);
SergGr
  • 23,570
  • 2
  • 30
  • 51
  • Thank you for your answer and precious time for this. This required me lot of time to implement this in my small application. Let me try this and get back to you in some time. – David Mar 28 '17 at 07:32
  • @David, I'm not sure where exactly is your problem with integrating this solution. If you provide some example closer to your real code I might (or might not) be able to help with that as well. – SergGr Mar 28 '17 at 14:04
  • I still don't know how to implement in my application. But your code is working perfectly in fiddle. I will use of this in my app. Thanks for the answer. – David Mar 29 '17 at 04:43
  • @David, could you clarify what is the trouble with integrating it to your applicaiton. As a last resort I think I can implement this as a subclass of `Tag` class which should solve all your problems. – SergGr Mar 29 '17 at 04:47
  • I have a tag field as a item. I just wann know where should I write this code. `window.tagField = pnl.items.items[0];window.lstDom = window.tagField.itemList.el.dom;window.container = window.lstDom.parentElement;` – David Mar 29 '17 at 04:57
  • @David, take a look at my last update with inheritance-based solution. I expect that you should have no problems integrating it into your existing code. Let me know if you still have some issues. – SergGr Mar 29 '17 at 05:48
  • Thank you @SergGr, This is one of the best answer ever I saw on SOF :) – David Mar 29 '17 at 06:21
  • @SerGr This is not working in IE 11. I am debugging code. If you also know please share :) – David Mar 30 '17 at 07:29
  • @David, I have no access to IE 11 right now, but from some of my tests on oder IE I added a hack that hopefully should fix IE 11 as well. See overridden `onItemListClick` in my updated answer – SergGr Mar 30 '17 at 15:15
  • @Ya, It is restricted to only two values. After placing more than two values it is showing incorrect behavior. – David Mar 31 '17 at 06:21
  • @David, when you say "incorrect behavior" what exactly do you mean? It is really hard to guess. I have no easy access to IE 11 so can't see it. Also have you tried my updated code that might fix issue if the issue is "scrolling in IE automatically scrolls to the bottom and shows dropdown"? – SergGr Mar 31 '17 at 06:24
  • Incorrect behavior is restricting to only two values. selecting more than two values it is not working. It working only for two values. I am debugging your code to get some clue. :) – David Mar 31 '17 at 06:27
  • @David, as you still don't specify what the "incorrect behavior" is, I must give up by this point. – SergGr Mar 31 '17 at 06:28
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/139550/discussion-between-david-and-serggr). – David Mar 31 '17 at 06:30
  • When I select 3 values and click on down arrow It must show 1 and then 2 and 3 values. But It showing only 1 and 2nd vales. Click on third values it is not going to down,. This is my meaning of incorrect behavior. Ideally it should go to third values as well. It is going in chrome. – David Mar 31 '17 at 06:32
  • I am using your code in fiddler. https://fiddle.sencha.com/#view/editor&fiddle/1t9r Now after selection click on the down arrow. You will get difference in Chrome and IE. – David Apr 03 '17 at 05:59
  • @David, is it true that this fiddle does not use my latest inheritance-based approach and thus `onItemListClick` doesn't override a [built-in method of `Tag` field](https://docs.sencha.com/extjs/6.0.2/classic/Ext.form.field.Tag.html#method-onItemListClick)? Can you reproduce the issue with my actual last code with IE hack? – SergGr Apr 03 '17 at 13:07
  • in my fiddler I am using `onItemListClick`. And it is behaving the same what I am asking. Fiddler with the update code is hard to make. The IE hack is present in same fiddler. – David Apr 04 '17 at 12:06
  • @David, do you mean https://fiddle.sencha.com/#view/editor&fiddle/1t9r as "my fiddle"? If so, then you do **not** use `onItemListClick` properly. For it to work it should be a part of a sub-class as defined in the first piece of code in my answer. If I jsut copy my first piece of code and my second piece of code into new Sencha fiddle or into your fiddle instead of your code, it seems to work OK in IE: see https://fiddle.sencha.com/#view/editor&fiddle/1tek Can you reproduce your issue in this fiddle? – SergGr Apr 04 '17 at 21:28
  • I completely agree this is correct. But Don't know whats wrong in my system. I will figure it out. – David Apr 06 '17 at 06:45
  • @David, do you use **_exactly_** the same code as in my first snippet (i.e. inheritance-based with `onItemListClick` inside the class)? If so, it sounds strange but I'm not sure how I can help you without a reproducible example. – SergGr Apr 06 '17 at 18:30
  • Ya I know. Even I also not sure how to show this to you. I am appreciating all your effort whatever you made these days.Thanks !! – David Apr 07 '17 at 04:30
2

The setting for growMax should be a height in pixels. To allow space for 3 of these items to be selected with a reasonable amount of space for scrolling, you could try setting growMax=60. See updated fiddle.

chrisuae
  • 1,092
  • 7
  • 8
  • Thanks for answer. But this will increase the height. What if I have a fixed height.like something column widget and all. ALso I noticed when I click on scroll bottom down arrow, my tagfield menu is opening. That supposed to open only when I click on common arrow of tag field. – David Mar 22 '17 at 04:31
  • I want constant height. – David Mar 22 '17 at 04:51
  • This answer mentions very important point that growMax is the max height in pixel. Extjs Doc must be updated. Works like magic in Extjs 6.2.3. I'm not sure why do we have maxHeight config. – Vineet Jan 28 '20 at 16:13