1. #1
    Sencha User
    Join Date
    Sep 2009
    Location
    Denver
    Posts
    51
    Vote Rating
    2
    McQuack_82 will become famous soon enough

      0  

    Default I made some small improvement to itemselector and multiselect

    I made some small improvement to itemselector and multiselect


    I made some changes to bring back the to and from labels that used to exist in 4.0 versions

    I enclosed a screenshot with old state of itemselector new state of itemselector and purposed state and the needed code changes bellow. [ATTAC
    H=CONFIG]36881[/ATTACH]

    Code:
    MultiSelect.js
    
    /**
    * A control that allows selection of multiple items in a list
    */
    Ext.define('Ext.ux.form.MultiSelect', {
    
    
        extend: 'Ext.form.FieldContainer',
    
    
        mixins: {
            bindable: 'Ext.util.Bindable',
            field: 'Ext.form.field.Field'
        },
    
    
        alternateClassName: 'Ext.ux.Multiselect',
        alias: ['widget.multiselectfield', 'widget.multiselect'],
    
    
        requires: ['Ext.panel.Panel', 'Ext.view.BoundList'],
    
    
        uses: ['Ext.view.DragZone', 'Ext.view.DropZone'],
    
    
        /**
        * @cfg {String} [dragGroup=""] The ddgroup name for the MultiSelect DragZone.
        */
    
    
        /**
        * @cfg {String} [dropGroup=""] The ddgroup name for the MultiSelect DropZone.
        */
    
    
        /**
        * @cfg {String} [title=""] A title for the underlying panel.
        */
    
    
        /**
        * @cfg {Boolean} [ddReorder=false] Whether the items in the MultiSelect list are drag/drop reorderable.
        */
        ddReorder: false,
    
    
        /**
        * @cfg {Object/Array} tbar An optional toolbar to be inserted at the top of the control's selection list.
        * This can be a {@link Ext.toolbar.Toolbar} object, a toolbar config, or an array of buttons/button configs
        * to be added to the toolbar. See {@link Ext.panel.Panel#tbar}.
        */
    
    
        /**
        * @cfg {String} [appendOnly=false] True if the list should only allow append drops when drag/drop is enabled.
        * This is useful for lists which are sorted.
        */
        appendOnly: false,
    
    
        /**
        * @cfg {String} [displayField="text"] Name of the desired display field in the dataset.
        */
        displayField: 'text',
    
    
        /**
        * @cfg {String} [valueField="text"] Name of the desired value field in the dataset.
        */
    
    
        /**
        * @cfg {Boolean} [allowBlank=true] False to require at least one item in the list to be selected, true to allow no
        * selection.
        */
        allowBlank: true,
    
    
        /**
        * @cfg {Number} [minSelections=0] Minimum number of selections allowed.
        */
        minSelections: 0,
    
    
        /**
        * @cfg {Number} [maxSelections=Number.MAX_VALUE] Maximum number of selections allowed.
        */
        maxSelections: Number.MAX_VALUE,
    
    
        /**
        * @cfg {String} [blankText="This field is required"] Default text displayed when the control contains no items.
        */
        blankText: 'This field is required',
    
    
        /**
        * @cfg {String} [minSelectionsText="Minimum {0}item(s) required"] 
        * Validation message displayed when {@link #minSelections} is not met. 
        * The {0} token will be replaced by the value of {@link #minSelections}.
        */
        minSelectionsText: 'Minimum {0} item(s) required',
    
    
        /**
        * @cfg {String} [maxSelectionsText="Maximum {0}item(s) allowed"] 
        * Validation message displayed when {@link #maxSelections} is not met
        * The {0} token will be replaced by the value of {@link #maxSelections}.
        */
        maxSelectionsText: 'Minimum {0} item(s) required',
    
    
        /**
        * @cfg {String} [delimiter=","] The string used to delimit the selected values when {@link #getSubmitValue submitting}
        * the field as part of a form. If you wish to have the selected values submitted as separate
        * parameters rather than a single delimited parameter, set this to <tt>null</tt>.
        */
        delimiter: ',',
    
    
        /**
        * @cfg {Ext.data.Store/Array} store The data source to which this MultiSelect is bound (defaults to <tt>undefined</tt>).
        * Acceptable values for this property are:
        * <div class="mdetail-params"><ul>
        * <li><b>any {@link Ext.data.Store Store} subclass</b></li>
        * <li><b>an Array</b> : Arrays will be converted to a {@link Ext.data.ArrayStore} internally.
        * <div class="mdetail-params"><ul>
        * <li><b>1-dimensional array</b> : (e.g., <tt>['Foo','Bar']</tt>)<div class="sub-desc">
        * A 1-dimensional array will automatically be expanded (each array item will be the combo
        * {@link #valueField value} and {@link #displayField text})</div></li>
        * <li><b>2-dimensional array</b> : (e.g., <tt>[['f','Foo'],['b','Bar']]</tt>)<div class="sub-desc">
        * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo
        * {@link #valueField value}, while the value at index 1 is assumed to be the combo {@link #displayField text}.
        * </div></li></ul></div></li></ul></div>
        */
    
    
        ignoreSelectChange: 0,
    
    
        initComponent: function () {
            var me = this;
    
    
            me.bindStore(me.store, true);
            if (me.store.autoCreated) {
                me.valueField = me.displayField = 'field1';
                if (!me.store.expanded) {
                    me.displayField = 'field2';
                }
            }
    
    
            if (!Ext.isDefined(me.valueField)) {
                me.valueField = me.displayField;
            }
            Ext.apply(me, me.setupItems());
    
    
    
    
            me.callParent();
            me.initField();
            me.addEvents('drop');
        },
    
    
        setupItems: function () {
            var me = this;
    
    
            me.boundList = Ext.create('Ext.view.BoundList', {
                deferInitialRefresh: false,
                multiSelect: true,
                store: me.store,
                displayField: me.displayField,
                disabled: me.disabled
            });
    
    
            me.boundList.getSelectionModel().on('selectionchange', me.onSelectChange, me);
    
    
            return {
                layout: 'fit',
                labelAlign: 'top',
                fieldLabel: me.fieldLabel,
                tbar: me.tbar,
                items: me.boundList
            };
        },
    
    
        onSelectChange: function (selModel, selections) {
            if (!this.ignoreSelectChange) {
                this.setValue(selections);
            }
        },
    
    
        getSelected: function () {
            return this.boundList.getSelectionModel().getSelection();
        },
    
    
        // compare array values
        isEqual: function (v1, v2) {
            var fromArray = Ext.Array.from,
                i = 0,
                len;
    
    
            v1 = fromArray(v1);
            v2 = fromArray(v2);
            len = v1.length;
    
    
            if (len !== v2.length) {
                return false;
            }
    
    
            for (; i < len; i++) {
                if (v2[i] !== v1[i]) {
                    return false;
                }
            }
    
    
            return true;
        },
    
    
        afterRender: function () {
            var me = this;
    
    
            me.callParent();
            if (me.selectOnRender) {
                ++me.ignoreSelectChange;
                me.boundList.getSelectionModel().select(me.getRecordsForValue(me.value));
                --me.ignoreSelectChange;
                delete me.toSelect;
            }
    
    
            if (me.ddReorder && !me.dragGroup && !me.dropGroup) {
                me.dragGroup = me.dropGroup = 'MultiselectDD-' + Ext.id();
            }
    
    
            if (me.draggable || me.dragGroup) {
                me.dragZone = Ext.create('Ext.view.DragZone', {
                    view: me.boundList,
                    ddGroup: me.dragGroup,
                    dragText: '{0} Item{1}'
                });
            }
            if (me.droppable || me.dropGroup) {
                me.dropZone = Ext.create('Ext.view.DropZone', {
                    view: me.boundList,
                    ddGroup: me.dropGroup,
                    handleNodeDrop: function (data, dropRecord, position) {
                        var view = this.view,
                            store = view.getStore(),
                            records = data.records,
                            index;
    
    
                        // remove the Models from the source Store
                        data.view.store.remove(records);
    
    
                        index = store.indexOf(dropRecord);
                        if (position === 'after') {
                            index++;
                        }
                        store.insert(index, records);
                        view.getSelectionModel().select(records);
                        me.fireEvent('drop', me, records);
                    }
                });
            }
        },
    
    
        isValid: function () {
            var me = this,
                disabled = me.disabled,
                validate = me.forceValidation || !disabled;
    
    
    
    
            return validate ? me.validateValue(me.value) : disabled;
        },
    
    
        validateValue: function (value) {
            var me = this,
                errors = me.getErrors(value),
                isValid = Ext.isEmpty(errors);
    
    
            if (!me.preventMark) {
                if (isValid) {
                    me.clearInvalid();
                } else {
                    me.markInvalid(errors);
                }
            }
    
    
            return isValid;
        },
    
    
        markInvalid: function (errors) {
            // Save the message and fire the 'invalid' event
            var me = this,
                oldMsg = me.getActiveError();
            me.setActiveErrors(Ext.Array.from(errors));
            if (oldMsg !== me.getActiveError()) {
                me.updateLayout();
            }
        },
    
    
        /**
        * Clear any invalid styles/messages for this field.
        *
        * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
        * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
        * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
        */
        clearInvalid: function () {
            // Clear the message and fire the 'valid' event
            var me = this,
                hadError = me.hasActiveError();
            me.unsetActiveError();
            if (hadError) {
                me.updateLayout();
            }
        },
    
    
        getSubmitData: function () {
            var me = this,
                data = null,
                val;
            if (!me.disabled && me.submitValue && !me.isFileUpload()) {
                val = me.getSubmitValue();
                if (val !== null) {
                    data = {};
                    data[me.getName()] = val;
                }
            }
            return data;
        },
    
    
        /**
        * Returns the value that would be included in a standard form submit for this field.
        *
        * @return {String} The value to be submitted, or null.
        */
        getSubmitValue: function () {
            var me = this,
                delimiter = me.delimiter,
                val = me.getValue();
    
    
            return Ext.isString(delimiter) ? val.join(delimiter) : val;
        },
    
    
        getValue: function () {
            return this.value;
        },
    
    
        getRecordsForValue: function (value) {
            var me = this,
                records = [],
                all = me.store.getRange(),
                valueField = me.valueField,
                i = 0,
                allLen = all.length,
                rec,
                j,
                valueLen;
    
    
            for (valueLen = value.length; i < valueLen; ++i) {
                for (j = 0; j < allLen; ++j) {
                    rec = all[j];
                    if (rec.get(valueField) == value[i]) {
                        records.push(rec);
                    }
                }
            }
    
    
            return records;
        },
    
    
        setupValue: function (value) {
            var delimiter = this.delimiter,
                valueField = this.valueField,
                i = 0,
                out,
                len,
                item;
    
    
            if (Ext.isDefined(value)) {
                if (delimiter && Ext.isString(value)) {
                    value = value.split(delimiter);
                } else if (!Ext.isArray(value)) {
                    value = [value];
                }
    
    
                for (len = value.length; i < len; ++i) {
                    item = value[i];
                    if (item && item.isModel) {
                        value[i] = item.get(valueField);
                    }
                }
                out = Ext.Array.unique(value);
            } else {
                out = [];
            }
            return out;
        },
    
    
        setValue: function (value) {
            var me = this,
                selModel = me.boundList.getSelectionModel();
    
    
            // Store not loaded yet - we cannot set the value
            if (!me.store.getCount()) {
                me.store.on({
                    load: Ext.Function.bind(me.setValue, me, [value]),
                    single: true
                });
                return;
            }
    
    
            value = me.setupValue(value);
            me.mixins.field.setValue.call(me, value);
    
    
            if (me.rendered) {
                ++me.ignoreSelectChange;
                selModel.deselectAll();
                selModel.select(me.getRecordsForValue(value));
                --me.ignoreSelectChange;
            } else {
                me.selectOnRender = true;
            }
        },
    
    
        clearValue: function () {
            this.setValue([]);
        },
    
    
        onEnable: function () {
            var list = this.boundList;
            this.callParent();
            if (list) {
                list.enable();
            }
        },
    
    
        onDisable: function () {
            var list = this.boundList;
            this.callParent();
            if (list) {
                list.disable();
            }
        },
    
    
        getErrors: function (value) {
            var me = this,
                format = Ext.String.format,
                errors = [],
                numSelected;
    
    
            value = Ext.Array.from(value || me.getValue());
            numSelected = value.length;
    
    
            if (!me.allowBlank && numSelected < 1) {
                errors.push(me.blankText);
            }
            if (numSelected < me.minSelections) {
                errors.push(format(me.minSelectionsText, me.minSelections));
            }
            if (numSelected > me.maxSelections) {
                errors.push(format(me.maxSelectionsText, me.maxSelections));
            }
            return errors;
        },
    
    
        onDestroy: function () {
            var me = this;
    
    
            me.bindStore(null);
            Ext.destroy(me.dragZone, me.dropZone);
            me.callParent();
        },
    
    
        onBindStore: function (store) {
            var boundList = this.boundList;
    
    
            if (boundList) {
                boundList.bindStore(store);
            }
        }
    
    
    });
    
    ItemSelector.js
    
    /*
    * Note that this control will most likely remain as an example, and not as a core Ext form
    * control.  However, the API will be changing in a future release and so should not yet be
    * treated as a final, stable API at this time.
    */
    
    
    /**
    * A control that allows selection of between two Ext.ux.form.MultiSelect controls.
    */
    Ext.define('Ext.ux.form.ItemSelector', {
        extend: 'Ext.ux.form.MultiSelect',
        alias: ['widget.itemselectorfield', 'widget.itemselector'],
        alternateClassName: ['Ext.ux.ItemSelector'],
        requires: [
            'Ext.button.Button',
            'Ext.ux.form.MultiSelect'
        ],
    
    
        fromLabel: 'From',
    
    
        toLabel: 'To',
    
    
        /**
        * @cfg {Boolean} [hideNavIcons=false] True to hide the navigation icons
        */
        hideNavIcons: false,
    
    
        /**
        * @cfg {Array} buttons Defines the set of buttons that should be displayed in between the ItemSelector
        * fields. Defaults to <tt>['top', 'up', 'add', 'remove', 'down', 'bottom']</tt>. These names are used
        * to build the button CSS class names, and to look up the button text labels in {@link #buttonsText}.
        * This can be overridden with a custom Array to change which buttons are displayed or their order.
        */
        buttons: ['top', 'up', 'add', 'remove', 'down', 'bottom'],
    
    
        /**
        * @cfg {Object} buttonsText The tooltips for the {@link #buttons}.
        * Labels for buttons.
        */
        buttonsText: {
            top: "Move to Top",
            up: "Move Up",
            add: "Add to Selected",
            remove: "Remove from Selected",
            down: "Move Down",
            bottom: "Move to Bottom"
        },
    
    
        initComponent: function () {
            var me = this;
    
    
            me.ddGroup = me.id + '-dd';
            me.callParent();
    
    
            // bindStore must be called after the fromField has been created because
            // it copies records from our configured Store into the fromField's Store
            me.bindStore(me.store);
        },
    
    
        createList: function (fieldLabel) {
            var me = this;
    
    
            return Ext.create('Ext.ux.form.MultiSelect', {
                submitValue: false,
                flex: 1,
                fieldLabel: fieldLabel,
                dragGroup: me.ddGroup,
                dropGroup: me.ddGroup,
                store: {
                    model: me.store.model,
                    data: []
                },
                displayField: me.displayField,
                disabled: me.disabled,
                listeners: {
                    boundList: {
                        scope: me,
                        itemdblclick: me.onItemDblClick,
                        drop: me.syncValue
                    }
                }
            });
        },
    
    
        setupItems: function () {
            var me = this;
    
    
            me.fromField = me.createList(me.fromLabel);
            me.toField = me.createList(me.toLabel);
    
    
            return {
                layout: {
                    type: 'hbox',
                    align: 'stretch'
                },
                items: [
                    me.fromField,
                    {
                        xtype: 'container',
                        margins: '0 4',
                        width: 22,
                        layout: {
                            type: 'vbox',
                            pack: 'center'
                        },
                        items: me.createButtons()
                    },
                    me.toField
                ]
            };
        },
    
    
        createButtons: function () {
            var me = this,
                buttons = [];
    
    
            if (!me.hideNavIcons) {
                Ext.Array.forEach(me.buttons, function (name) {
                    buttons.push({
                        xtype: 'button',
                        tooltip: me.buttonsText[name],
                        handler: me['on' + Ext.String.capitalize(name) + 'BtnClick'],
                        cls: Ext.baseCSSPrefix + 'form-itemselector-btn',
                        iconCls: Ext.baseCSSPrefix + 'form-itemselector-' + name,
                        navBtn: true,
                        scope: me,
                        margin: '4 0 0 0'
                    });
                });
            }
            return buttons;
        },
    
    
        /**
        * Get the selected records from the specified list.
        * 
        * Records will be returned *in store order*, not in order of selection.
        * @param {Ext.view.BoundList} list The list to read selections from.
        * @return {Ext.data.Model[]} The selected records in store order.
        * 
        */
        getSelections: function (list) {
            var store = list.getStore();
    
    
            return Ext.Array.sort(list.getSelectionModel().getSelection(), function (a, b) {
                a = store.indexOf(a);
                b = store.indexOf(b);
    
    
                if (a < b) {
                    return -1;
                } else if (a > b) {
                    return 1;
                }
                return 0;
            });
        },
    
    
        onTopBtnClick: function () {
            var list = this.toField.boundList,
                store = list.getStore(),
                selected = this.getSelections(list);
    
    
            store.suspendEvents();
            store.remove(selected, true);
            store.insert(0, selected);
            store.resumeEvents();
            list.refresh();
            this.syncValue();
            list.getSelectionModel().select(selected);
        },
    
    
        onBottomBtnClick: function () {
            var list = this.toField.boundList,
                store = list.getStore(),
                selected = this.getSelections(list);
    
    
            store.suspendEvents();
            store.remove(selected, true);
            store.add(selected);
            store.resumeEvents();
            list.refresh();
            this.syncValue();
            list.getSelectionModel().select(selected);
        },
    
    
        onUpBtnClick: function () {
            var list = this.toField.boundList,
                store = list.getStore(),
                selected = this.getSelections(list),
                rec,
                i = 0,
                len = selected.length,
                index = 0;
    
    
            // Move each selection up by one place if possible
            store.suspendEvents();
            for (; i < len; ++i, index++) {
                rec = selected[i];
                index = Math.max(index, store.indexOf(rec) - 1);
                store.remove(rec, true);
                store.insert(index, rec);
            }
            store.resumeEvents();
            list.refresh();
            this.syncValue();
            list.getSelectionModel().select(selected);
        },
    
    
        onDownBtnClick: function () {
            var list = this.toField.boundList,
                store = list.getStore(),
                selected = this.getSelections(list),
                rec,
                i = selected.length - 1,
                index = store.getCount() - 1;
    
    
            // Move each selection down by one place if possible
            store.suspendEvents();
            for (; i > -1; --i, index--) {
                rec = selected[i];
                index = Math.min(index, store.indexOf(rec) + 1);
                store.remove(rec, true);
                store.insert(index, rec);
            }
            store.resumeEvents();
            list.refresh();
            this.syncValue();
            list.getSelectionModel().select(selected);
        },
    
    
        onAddBtnClick: function () {
            var me = this,
                selected = me.getSelections(me.fromField.boundList);
    
    
            me.moveRec(true, selected);
            me.toField.boundList.getSelectionModel().select(selected);
        },
    
    
        onRemoveBtnClick: function () {
            var me = this,
                selected = me.getSelections(me.toField.boundList);
    
    
            me.moveRec(false, selected);
            me.fromField.boundList.getSelectionModel().select(selected);
        },
    
    
        moveRec: function (add, recs) {
            var me = this,
                fromField = me.fromField,
                toField = me.toField,
                fromStore = add ? fromField.store : toField.store,
                toStore = add ? toField.store : fromField.store;
    
    
            fromStore.suspendEvents();
            toStore.suspendEvents();
            fromStore.remove(recs);
            toStore.add(recs);
            fromStore.resumeEvents();
            toStore.resumeEvents();
    
    
            fromField.boundList.refresh();
            toField.boundList.refresh();
    
    
            me.syncValue();
        },
    
    
        // Synchronizes the submit value with the current state of the toStore
        syncValue: function () {
            var me = this;
            me.mixins.field.setValue.call(me, me.setupValue(me.toField.store.getRange()));
        },
    
    
        onItemDblClick: function (view, rec) {
            this.moveRec(view === this.fromField.boundList, rec);
        },
    
    
        setValue: function (value) {
            var me = this,
                fromField = me.fromField,
                toField = me.toField,
                fromStore = fromField.store,
                toStore = toField.store,
                selected;
    
    
            // Wait for from store to be loaded
            if (!me.fromStorePopulated) {
                me.fromField.store.on({
                    load: Ext.Function.bind(me.setValue, me, [value]),
                    single: true
                });
                return;
            }
    
    
            value = me.setupValue(value);
            me.mixins.field.setValue.call(me, value);
    
    
            selected = me.getRecordsForValue(value);
    
    
            // Clear both left and right Stores.
            // Both stores must not fire events during this process.
            fromStore.suspendEvents();
            toStore.suspendEvents();
            fromStore.removeAll();
            toStore.removeAll();
    
    
            // Reset fromStore
            me.populateFromStore(me.store);
    
    
            // Copy selection across to toStore
            Ext.Array.forEach(selected, function (rec) {
                // In the from store, move it over
                if (fromStore.indexOf(rec) > -1) {
                    fromStore.remove(rec);
                }
                toStore.add(rec);
            });
    
    
            // Stores may now fire events
            fromStore.resumeEvents();
            toStore.resumeEvents();
    
    
            // Refresh both sides and then update the app layout
            Ext.suspendLayouts();
            fromField.boundList.refresh();
            toField.boundList.refresh();
            Ext.resumeLayouts(true);
        },
    
    
        onBindStore: function (store, initial) {
            var me = this;
    
    
            if (me.fromField) {
                me.fromField.store.removeAll()
                me.toField.store.removeAll();
    
    
                // Add everything to the from field as soon as the Store is loaded
                if (store.getCount()) {
                    me.populateFromStore(store);
                } else {
                    me.store.on('load', me.populateFromStore, me);
                }
            }
        },
    
    
        populateFromStore: function (store) {
            var fromStore = this.fromField.store;
    
    
            // Flag set when the fromStore has been loaded
            this.fromStorePopulated = true;
    
    
            fromStore.add(store.getRange());
    
    
            // setValue waits for the from Store to be loaded
            fromStore.fireEvent('load', fromStore);
        },
    
    
        onEnable: function () {
            var me = this;
    
    
            me.callParent();
            me.fromField.enable();
            me.toField.enable();
    
    
            Ext.Array.forEach(me.query('[navBtn]'), function (btn) {
                btn.enable();
            });
        },
    
    
        onDisable: function () {
            var me = this;
    
    
            me.callParent();
            me.fromField.disable();
            me.toField.disable();
    
    
            Ext.Array.forEach(me.query('[navBtn]'), function (btn) {
                btn.disable();
            });
        },
    
    
        onDestroy: function () {
            this.bindStore(null);
            this.callParent();
        }
    });
    Attached Images
    Last edited by scottmartin; 6 Jul 2012 at 8:39 AM. Reason: Please format your code

  2. #2
    Sencha - Support Team scottmartin's Avatar
    Join Date
    Jul 2010
    Location
    Houston, Tx
    Posts
    8,927
    Vote Rating
    443
    scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future scottmartin has a brilliant future

      0  

    Default


    Thank you for the contribution. We have had several requests for this.

    Scott.

Thread Participants: 1

Tags for this Thread

Turkiyenin en sevilen filmlerinin yer aldigi xnxx internet sitemiz olan ve porn sex tarzi bir site olan mobil porno izle sitemiz gercekten dillere destan bir durumda herkesin sevdigi bir site olarak tarihe gececege benziyor. Sitenin en belirgin ozelliklerinden birisi de Turkiyede gercekten kaliteli ve muntazam, duzenli porno izle siteleri olmamasidir. Bu yuzden iste. Ayrica en net goruntu kalitesine sahip adresinde yayinlanmaktadir. Mesela diğer sitelerimizden bahsedecek olursak, en iyi hd porno video arşivine sahip bir siteyiz. "The Best anal porn videos and slut anus, big asses movies set..." hd porno faketaxi