0
votes

I'm using the latest Handsontable version 9.0.0 without any framework and I'm trying to follow the Cell editor developer guide, but I'm at a loss.

My requirement is to show a couple of checkboxes and a text box in one cell (not my idea). My thought was to have the data for the cell be a little json string {"Attr1": true, "Attr2": false} and have a custom renderer/editor that would parse the cell value and set the checkboxes appropriately.

I made a fiddle of it here: http://jsfiddle.net/9k1x4z6b/2/

I created a class for the custom attributes column and a renderer function and set the renderer and editor for the column like this

    class CustomAttributesEditor extends Handsontable.editors.BaseEditor {
    /**
    * Initializes editor instance, DOM Element and mount hooks.
    */

    // constructor (props) {
    //     super(props)
    // }

    prepare(row, col, prop, td, originalValue, cellProperties) {
        // Invoke the original method...
        super.prepare(row, col, prop, td, originalValue, cellProperties);

        td.innerHTML = '';

        this.AttributeNames = ['Attr1', 'Attr2'];
        this.ctrls = {};
        let parsedValue = JSON.parse(Handsontable.helper.stringify(originalValue));

        // Create checkbox controls
        for (let i = 0; i < this.AttributeNames.length; i++) {
            let AttributeName = this.AttributeNames[i];

            let span = document.createElement('span');
            span.style.whiteSpace = 'nowrap';

            let checkbox = document.createElement('input');
            this.ctrls[AttributeName] = checkbox;
            checkbox.type = 'checkbox';

            if (parsedValue[AttributeName] == 'yes') {
                checkbox.checked = true;
            }
            
            let label = document.createElement('label');
            label.innerHTML = AttributeName;
            label.htmlFor = checkbox.id;

            span.appendChild(checkbox);
            span.appendChild(label);
            td.appendChild(span);
            td.appendChild(document.createElement('br'));
        }

        // Create a control that is shown/hidden when the "Attr2" checkbox is toggled
        let CustomAttributesAttr3SubDiv = document.createElement('div');
        var label = document.createElement('label');
        label.innerHTML = "Attr3 supplier:";
        CustomAttributesAttr3SubDiv.appendChild(label);
        
        var CustomAttributesAttr3 = document.createElement('input');
        if (parsedValue.hasOwnProperty('Attr3')) {
            CustomAttributesAttr3.value = parsedValue['Attr3'];
        }
        this.ctrls['Attr3'] = CustomAttributesAttr3;
        this.AttributeNames.push('Attr3');
        CustomAttributesAttr3.setAttribute('title', 'Attr3');
        CustomAttributesAttr3.style.width = '12em';

        CustomAttributesAttr3SubDiv.appendChild(CustomAttributesAttr3);
        CustomAttributesAttr3SubDiv.appendChild(document.createElement('br'));
        td.appendChild(CustomAttributesAttr3SubDiv);

        let Attr2Checkbox = this.ctrls['Attr2'];
        //CustomAttributes_ShowHideValueCtrl(Attr2Checkbox);
        $(Attr2Checkbox).off('change').on('change', function () {
            //CustomAttributes_ShowHideValueCtrl(this); // irrelevant to checkbox problem. function shows Attr3 input when Attr2Checkbox is checked, hides otherwise
        });

        //preventDefault();
    }

    getValue(){
        // This function returns the set value of the controls
        let ctrls = this.ctrls;
        let resultDict = {};
        for (let ctrlID in ctrls){
            let ctrl = ctrls[ctrlID];
            let FormattedAttributeName = ctrlID.replaceAll(' ', '_');
            let val = null;
            if (ctrl.type == 'checkbox'){
                if (ctrl.checked == true) {
                    val = 'yes';
                } else {
                    val = null;
                }
            } else {
                val = ctrl.value;
            }
            resultDict[FormattedAttributeName] = val;
        }

        return JSON.stringify(resultDict)
    }

    setValue(value){
        // this function sets the value of the controls to match the data value

        let parsedValue = {};
        try {
            parsedValue = JSON.parse(Handsontable.helper.stringify(value));
        } catch (exc) {
            for (let i = 0; i < this.AttributeNames.length; i++) {
                parsedValue[this.AttributeNames[i]] = 'no';
            }
        }

        let ctrls = this.ctrls;
        let resultDict = {};
        for (let ctrlID in ctrls){
            let ctrl = ctrls[ctrlID];
            let FormattedAttributeName = ctrlID.replaceAll(' ', '_');
            let val = parsedValue[FormattedAttributeName];
            if (ctrl.type == 'checkbox'){
                if (val == 'yes'){
                    ctrl.checked = true;
                } else {
                    ctrl.checked = false;
                }
            } else {
                ctrl.value = val;
            }
        }
        
    }

    saveValue(value, ctrlDown){
        super.saveValue(value, ctrlDown);
    }

    open(){}
    close(){}
    focus(){}
}

function CustomAttributesRenderer(instance, td, row, col, prop, value, cellProperties) {
    // This function shows labels for the checked Attr1-3 values
    let AttributeNames = ['Attr1', 'Attr2', 'Attr3'];
    parsedValue = JSON.parse(Handsontable.helper.stringify(value));
    
    Handsontable.dom.empty(td);
    for (let i = 0; i < AttributeNames.length; i++) {
        let AttributeName = AttributeNames[i];

        let span = document.createElement('span');
        span.style.whiteSpace = 'nowrap';

        if (parsedValue[AttributeName] == 'yes') {
          let label = document.createElement('label');
            label.innerHTML = AttributeName;
          span.appendChild(label);
            td.appendChild(span);
        }

        td.appendChild(document.createElement('br'));
    }

    return td;
}


document.addEventListener("DOMContentLoaded", function () {
    var container = document.getElementById('divBFEPartMatrix');

    var hot = new Handsontable(container, {
        data: [
            [JSON.stringify({"Attr1": "yes", "Attr2": "yes", "Attr3": ""})],
            [JSON.stringify({"Attr1": "yes", "Attr2": "yes", "Attr3": "somevalue"})],
            [JSON.stringify({"Attr1": "no", "Attr2": "no", "Attr3": ""})],
        ],
        columns: [
            {renderer: CustomAttributesRenderer, editor: CustomAttributesEditor}
        ],
        rowHeaders: true,
        colHeaders: true,
        filters: true,
        dropdownMenu: true
    });
})

The result appears properly, with the cell initially not having checkboxes visible due to the renderer not showing them, then when you click the cell the checkboxes appear. The problem is when you click the checkbox it doesn't toggle.

I presume something in handsontable is re-creating the td and blowing away the state change, but I can't figure out how to prevent that from happening. Is there a fully working custom editor fiddle or something I can reference to figure out how to prevent bubbling?

Any help you can offer would be greatly appreciated.

1

1 Answers

0
votes

Well, after another couple days of fiddling around I figured out the full set of code I needed to add to make this work. Posted here in case it helps someone else trying to make a custom cell editor/renderer in handsontable.

class SubstitutePartEditor extends Handsontable.editors.BaseEditor {
    init(){
        // This function creates the edit div
        let div = document.createElement('div');
        this.div = div;
        div.style.display = 'none';
        div.style.position = 'absolute';
        div.style.width = 'auto';
        div.style.backgroundColor = 'white';

        this.AttributeNames = ['The waste bin is part of the waste cart', 'This item includes SUPPLIER tapestry'];
        this.ctrls = {};


        let cbIsSubstitutePart = document.createElement('input');
        this.ctrls['IsSubstitutePart'] = cbIsSubstitutePart;
        cbIsSubstitutePart.type = 'checkbox';
        div.appendChild(cbIsSubstitutePart);

        div.appendChild(document.createElement('br'));
        let SubstitutePartSubDiv = document.createElement('div');
        SubstitutePartSubDiv.style.display = 'inline-block';
        div.appendChild(SubstitutePartSubDiv);

        let inputSubstitutePart = document.createElement('textarea');
        this.ctrls['SubstitutePart'] = inputSubstitutePart;
        inputSubstitutePart.style.width = '220px';
        inputSubstitutePart.style.height = '88px';
        SubstitutePartSubDiv.appendChild(inputSubstitutePart);

        this.hot.rootElement.appendChild(div);
    }

    UpdateDependentControls(){
        let RequiredCtrl = this.ctrls['IsSubstitutePart'];
        let Ctrl = this.ctrls['SubstitutePart'];

        if (RequiredCtrl.checked == true){
            Ctrl.style.display = '';
        } else {
            Ctrl.style.display = 'none';
        }

        $(RequiredCtrl).off('change').on('change', function(){
            if (RequiredCtrl.checked == true){
                Ctrl.style.display = '';
            } else {
                Ctrl.style.display = 'none';
            }
        });

    }

    getValue(){
        // This function returns the set value of the controls
        let ctrls = this.ctrls;
        let resultDict = {};
        for (let ctrlID in ctrls){
            let ctrl = ctrls[ctrlID];
            let FormattedAttributeName = ctrlID.replaceAll(' ', '_');
            let val = null;
            if (ctrl.type == 'checkbox'){
                if (ctrl.checked == true) {
                    val = 'yes';
                } else {
                    val = null;
                }
            } else {
                val = ctrl.value;
            }
            resultDict[FormattedAttributeName] = val;
        }

        return JSON.stringify(resultDict)
    }

    setValue(value){
        // this function sets the value of the controls to match the data value
        let parsedValue = {};
        try {
            parsedValue = JSON.parse(Handsontable.helper.stringify(value));
        } catch (exc) {
            parsedValue = {
                IsSubstitutePart: 'no',
                SubstitutePart: "This item requires a waiver from the operator's foreign regulatory agency, <FOREIGN REGULATORY AGENCY NAME>."
            };
        }

        let ctrls = this.ctrls;
        let resultDict = {};
        for (let ctrlID in ctrls){
            let ctrl = ctrls[ctrlID];
            let FormattedAttributeName = ctrlID.replaceAll(' ', '_');
            let val = parsedValue[FormattedAttributeName];
            if (ctrl.type == 'checkbox'){
                if (val == 'yes'){
                    ctrl.checked = true;
                } else {
                    ctrl.checked = false;
                }
            } else {
                ctrl.value = val;
            }
        }
        
    }

    saveValue(value, ctrlDown){
        super.saveValue(value, ctrlDown);
    }

    open() {
      this._opened = true;
      this.refreshDimensions();
      this.UpdateDependentControls();
      this.div.style.display = '';
    }

    refreshDimensions() {
        this.TD = this.getEditedCell();

        // TD is outside of the viewport.
        if (!this.TD) {
            this.close();
            return;
        }

        const { wtOverlays } = this.hot.view.wt;
        const currentOffset = Handsontable.dom.offset(this.TD);
        const containerOffset = Handsontable.dom.offset(this.hot.rootElement);
        const scrollableContainer = wtOverlays.scrollableElement;
        const editorSection = this.checkEditorSection();
        let width = Handsontable.dom.outerWidth(this.TD) + 1;
        let height = Handsontable.dom.outerHeight(this.TD) + 1;
        let editTop = currentOffset.top - containerOffset.top - 1 - (scrollableContainer.scrollTop || 0);
        let editLeft = currentOffset.left - containerOffset.left - 1 - (scrollableContainer.scrollLeft || 0);
        let cssTransformOffset;

        switch (editorSection) {
        case 'top':
          cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.topOverlay.clone.wtTable.holder.parentNode);
          break;
        case 'left':
          cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.leftOverlay.clone.wtTable.holder.parentNode);
          break;
        case 'top-left-corner':
          cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.topLeftCornerOverlay.clone.wtTable.holder.parentNode);
          break;
        case 'bottom-left-corner':
          cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.bottomLeftCornerOverlay.clone.wtTable.holder.parentNode);
          break;
        case 'bottom':
          cssTransformOffset = Handsontable.dom.getCssTransform(wtOverlays.bottomOverlay.clone.wtTable.holder.parentNode);
          break;
        default:
          break;
        }

        if (this.hot.getSelectedLast()[0] === 0) {
            editTop += 1;
        }
        if (this.hot.getSelectedLast()[1] === 0) {
            editLeft += 1;
        }

        const selectStyle = this.div.style;

        if (cssTransformOffset && cssTransformOffset !== -1) {
            selectStyle[cssTransformOffset[0]] = cssTransformOffset[1];
        } else {
            Handsontable.dom.resetCssTransform(this.div);
        }

        const cellComputedStyle = Handsontable.dom.getComputedStyle(this.TD, this.hot.rootWindow);

        if (parseInt(cellComputedStyle.borderTopWidth, 10) > 0) {
            height -= 1;
        }
        if (parseInt(cellComputedStyle.borderLeftWidth, 10) > 0) {
            width -= 1;
        }

        selectStyle.height = `${height}px`;
        selectStyle.minWidth = `${width}px`;
        selectStyle.top = `${editTop}px`;
        selectStyle.left = `${editLeft}px`;
        selectStyle.margin = '0px';
    }

    getEditedCell() {
        const { wtOverlays } = this.hot.view.wt;
        const editorSection = this.checkEditorSection();
        let editedCell;

        switch (editorSection) {
            case 'top':
                editedCell = wtOverlays.topOverlay.clone.wtTable.getCell({
                    row: this.row,
                    col: this.col
                });
                this.select.style.zIndex = 101;
                break;
            case 'corner':
                editedCell = wtOverlays.topLeftCornerOverlay.clone.wtTable.getCell({
                    row: this.row,
                    col: this.col
                });
                this.select.style.zIndex = 103;
                break;
            case 'left':
                editedCell = wtOverlays.leftOverlay.clone.wtTable.getCell({
                    row: this.row,
                    col: this.col
                });
                this.select.style.zIndex = 102;
                break;
            default:
                editedCell = this.hot.getCell(this.row, this.col);
                this.div.style.zIndex = '';
                break;
        }

        return editedCell < 0 ? void 0 : editedCell; 
    }

    focus() {
        this.div.focus();
    }

    close() {
        this._opened = false;
        this.div.style.display = 'none';
    }
}
function SubstitutePartRenderer(instance, td, row, col, prop, value, cellProperties) {
    // This function draws the multi checkboxes for the SubstitutePart input field
    // Note: if AttributeNames changes you must also update BFEPartMatrix_Edit.ascx line ~240 to match (<- this is where the data is saved)
    //Handsontable.renderers.HtmlRenderer.apply(this, arguments);

    let parsedValue = {};
    try {
        parsedValue = JSON.parse(Handsontable.helper.stringify(value));
    } catch {
        // nothing to do
    }

    Handsontable.dom.empty(td);

    let div = document.createElement('div');
    //div.style.whiteSpace = 'nowrap';
    div.style.display = 'block';
    td.appendChild(div);

    if (parsedValue.hasOwnProperty('IsSubstitutePart')) {
        if (parsedValue.IsSubstitutePart == 'yes') {
            
        } else {
            td.innerHTML = 'N/A';
            return;
        }
    } else {
        td.innerHTML = 'N/A';
        return;
    }

    let SubstitutePartSubDiv = document.createElement('div');
    SubstitutePartSubDiv.style.display = 'inline-block';
    div.appendChild(SubstitutePartSubDiv);

    // text area
    let inputSubstitutePart = document.createElement('label');
    inputSubstitutePart.innerHTML = parsedValue['SubstitutePart'].escape();
    inputSubstitutePart.style.width = '220px';
    inputSubstitutePart.style.height = '88px';
    SubstitutePartSubDiv.appendChild(inputSubstitutePart);

    return td;
}

then set the render and editor of the hot column like this

columns: [
        {renderer: CustomAttributesRenderer, editor: CustomAttributesEditor}
    ],