替换 editable DataTable 中的初始数据框并在另一个 table 中使用该新数据框

Replacing initial Dataframe from an editable DataTable and using that new data frame in another table

我的 Shiny 应用程序中有一个嵌套的 DataTable。 child table 中的一些列被用户编辑table。这里的目标是让用户编辑值,然后数据框将被这些新值替换。然后我想将替换的数据框用于另一个 table。我需要在另一个 table.

中使用编辑后的值进行计算

工作流程:

create nested data table -> user edits values -> initial data is replaced by new data -> use the new data for another table

我尝试使用 reactiveValues()dataTableProxy()replaceData()observeEvert() 来遵循该工作流程,但没有成功。

# Parent table
structure(list(Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"
), `Gross CPP` = c(".94", ".89"), `Gross CPM` = c(".02", 
"[=12=].82"), `Historical Composite Gross CPP (if applicable)` = c("[=12=]", 
"[=12=]"), `Historical Composite Gross CPM (if applicable)` = c("[=12=]", 
"[=12=]")), .Names = c("Market", "Gross CPP", "Gross CPM", "Historical Composite Gross CPP (if applicable)", 
"Historical Composite Gross CPM (if applicable)"), row.names = c(NA, 
-2L), class = "data.frame")

# Child table
structure(list(Market = c("ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ABILENE-SWEETWATER", "ABILENE-SWEETWATER", "ABILENE-SWEETWATER", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY", "ALBANY-SCHENECTADY-TROY, NY", 
"ALBANY-SCHENECTADY-TROY, NY"), Daypart = c("Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen", "Daytime", "Early Fringe", 
"Early Morning", "Early News", "Late Fringe", "Late News", "Prime Access", 
"Prime Time", "tv_2", "tv_3", "tv_cross_screen"), `Mix (%)` = c(15, 
10, 15, 10, 5, 5, 10, 10, 0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 
10, 0, 0, 0), `Spot:30 (%)` = c(15, 10, 15, 10, 5, 5, 10, 10, 
0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 10, 0, 0, 0), `Spot:15 (%)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Gross CPP ($)` = c(18, 18, 16, 23, 24, 40, 26, 44, 0, 0, 
0, 77, 71, 61, 78, 109, 145, 93, 213, 0, 0, 0), `Gross CPM ($)` = c(1.57, 
1.57, 1.39, 2, 2.09, 3.49, 2.27, 3.83, 23, 21, 13, 6.71, 6.19, 
5.32, 6.8, 9.5, 12.63, 8.1, 18.56, 23, 21, 13), `Historical Composite CPP ($)` = c(0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
), `Historical Composite CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0, 
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)), .Names = c("Market", 
"Daypart", "Mix (%)", "Spot:30 (%)", "Spot:15 (%)", "Gross CPP ($)", 
"Gross CPM ($)", "Historical Composite CPP ($)", "Historical Composite CPM ($)"
), class = "data.frame", row.names = c(NA, -22L))

  # Module to create the nested structure of the table
  NestedData <- function(dat, children) {
    stopifnot(length(children) == nrow(dat))
    g <- function(d){
      if(is.data.frame(d)){
        purrr::transpose(d)
      }else{
        purrr::transpose(NestedData(d[[1]], children = d$children))
      }
    }
    subdats <- lapply(children, g)
    oplus <- sapply(subdats, function(x) if(length(x)) "<img src=\'https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\'/>" else "")
    cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
  }
  # Bind the market level and mix breakout data together for the final table
  market_mix_table <- reactive({
    # Take a dependency on input$goButton
    input$goButton
    isolate({
      # The rolled up market view - Parent table
      parent <- market_level_view()
      # The daypart breakout for each market - Child table
      child <- mix_breakout_digital_elements()
      # Make the dataframe
      # This must be met length(children) == nrow(dat)
      Dat <- NestedData(
        dat = parent,
        children = split(child, child$Market)
      )
      return(Dat)
    })
  })
  # Render the table
  output$daypartTable <- DT::renderDataTable({
    # Whether to show row names (set TRUE or FALSE)
    rowNames <- FALSE
    colIdx <- as.integer(rowNames)
    # The data
    Dat <- market_mix_table()
    parentRows <- which(Dat[,1] != "")
    excelTitle <- paste(
      input$name,
      input$medium,
      input$quarter,
      "Market CPM-CPP Breakout",
      sep=" "
    )
    ## If the JS stops working take the coed and put it here
    callback_js = JS(
      "var ok = true;",
      "function onUpdate(updatedCell, updatedRow, oldValue) {",
      "  var column = updatedCell.index().column;",
      "  if(column === 8){",
      "    ok = false;",
      "  }else if(column === 7){",
      "    ok = true;",
      "  }",
      "}",
      sprintf("var parentRows = [%s];", toString(parentRows-1)),
      sprintf("var j0 = %d;", colIdx),
      "var nrows = table.rows().count();",
      "for(var i=0; i < nrows; ++i){",
      "  if(parentRows.indexOf(i) > -1){",
      "    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
      "  }else{",
      "    table.cell(i,j0).nodes().to$().removeClass('details-control');",
      "  }",
      "}",
      "",
      "// make the table header of the nested table",
      "var format = function(d, childId){",
      "  if(d != null){",
      "    var html = ",
      "      '<table class=\"display compact hover\" ' + ",
      "      'style=\"padding-left: 30px;\" id=\"' + childId + '\"><thead><tr>';",
      "    for(var key in d[d.length-1][0]){",
      "      html += '<th>' + key + '</th>';",
      "    }",
      "    html += '</tr></thead><tfoot><tr>'",
      "    for(var key in d[d.length-1][0]){",
      "      html += '<th></th>';",
      "    }",
      "    return html + '</tr></tfoot></table>';",
      "  } else {",
      "    return '';",
      "  }",
      "};",
      "",
      "// row callback to style the rows of the child tables",
      "var rowCallback = function(row, dat, displayNum, index){",
      "  if($(row).hasClass('odd')){",
      "    $(row).css('background-color', 'white');",
      "    $(row).hover(function(){",
      "      $(this).css('background-color', 'lightgreen');",
      "    }, function() {",
      "      $(this).css('background-color', 'white');",
      "    });",
      "  } else {",
      "    $(row).css('background-color', 'white');",
      "    $(row).hover(function(){",
      "      $(this).css('background-color', 'lightblue');",
      "    }, function() {",
      "      $(this).css('background-color', 'white');",
      "    });",
      "  }",
      "};",
      "",
      "// header callback to style the header of the child tables",
      "var headerCallback = function(thead, data, start, end, display){",
      "  $('th', thead).css({",
      "    'color': 'black',",
      "    'background-color': 'white'",
      "  });",
      "};",
      "",
      "// make the datatable",
      "var format_datatable = function(d, childId, rowIdx){",
      "  // footer callback to display the totals",
      "  // and update the parent row",
      "  var footerCallback = function(tfoot, data, start, end, display){",
      "    $('th', tfoot).css('background-color', '#F5F2F2');",
      "    var api = this.api();",
      "// update the Override CPM when the Override CPP is changed",
      "    var col_override_cpp = api.column(7).data();",
      "    var col_population = api.column(9).data();",
      "    if(ok){",
      "      for(var i = 0; i < col_override_cpp.length; i++){",
      "        api.cell(i,8).data(((parseFloat(col_override_cpp[i])*100)/(parseFloat(col_population[i])/1000)).toFixed(2));",
      "      }",
      "    }",
      "// update the Override CPP when the Override CPM is changed",
      "    var col_override_cpm = api.column(8).data();",
      "    for(var i = 0; i < col_override_cpm.length; i++){",
      "      api.cell(i,7).data(((parseFloat(col_override_cpm[i])*parseFloat(col_population[i])/1000)/100).toFixed(0));",
      "    }",
      "// Update the spot mixes",
      "    var col_mix_percentage = api.column(2).data();",
      "    var col_mix60_mix30 = api.column(10).data();",
      "    var col_mix30_mix15 = api.column(11).data();",
      "    for(var i = 0; i < col_mix_percentage.length; i++){",
      "      api.cell(i,3).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix60_mix30[i])).toFixed(1));",
      "      api.cell(i,4).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix30_mix15[i])).toFixed(1));",
      "    }",
      "    var child_col_CPM = api.column(6).data();",
      "    for(var i = 0; i < child_col_CPM.length; i++){",
      "      api.cell(i,6).data(parseFloat(child_col_CPM[i]).toFixed(2));",
      "    }",
      # "    parseFloat(api.column(5)).toFixed(2)",
      "// Make the footer sums",
      "    api.columns().eq(0).each(function(index){",
      "      if(index == 0) return $(api.column(index).footer()).html('Mix Total');",
      "      var coldata = api.column(index).data();",
      "      var total = coldata",
      "          .reduce(function(a, b){return parseInt(a) + parseInt(b)}, 0);",
      "      if(index == 3 || index == 4 ||index == 5 || index == 6 || index == 7 || index == 8) {",
      "        $(api.column(index).footer()).html('');",
      "      } else {",
      "        $(api.column(index).footer()).html(total);",
      "      }",
      "      if(total == 100) {",
      "        $(api.column(index).footer()).css({'color': 'green'});",
      "      } else {",
      "        $(api.column(index).footer()).css({'color': 'red'});",
      "      }",
      "    })",
      "  // update the parent row",
      "    var col_share = api.column(2).data();",
      "    var col_CPP = api.column(5).data();",
      "    var col_CPM = api.column(6).data();",
      "    var col_Historical_CPP = api.column(7).data();",
      "    var col_Historical_CPM = api.column(8).data();",
      "    var CPP = 0, CPM = 0, Historical_CPP = 0, Historical_CPM = 0;",
      "    for(var i = 0; i < col_share.length; i++){",
      "      CPP += (parseInt(col_share[i])*parseInt(col_CPP[i]).toFixed(0));",
      "      CPM += (parseInt(col_share[i])*parseInt(col_CPM[i]).toFixed(2));",
      "      Historical_CPP += (parseInt(col_share[i])*parseInt(col_Historical_CPP[i]).toFixed(0));",
      "      Historical_CPM += (parseInt(col_share[i])*parseInt(col_Historical_CPM[i]).toFixed(2));",
      "    }",
      "    table.cell(rowIdx, j0+3).data((CPP/100).toFixed(2));",
      "    table.cell(rowIdx, j0+4).data((CPM/100).toFixed(2));",
      "    table.cell(rowIdx, j0+5).data((Historical_CPP/100).toFixed(2));",
      "    table.cell(rowIdx, j0+6).data((Historical_CPM/100).toFixed(2));",
      "  }",
      "  var n = d.length - 1;",
      "  var id = 'table#' + childId;",
      "  var columns = Object.keys(d[n][0]).map(function(x){",
      "    return {data: x, title: x};",
      "  });",
      "  if (Object.keys(d[n][0]).indexOf('_details') === -1) {",
      "    var subtable = $(id).DataTable({",
      "                 'data': d[n],",
      "                 'columns': columns,",
      "                 'autoWidth': true,",
      "                 'deferRender': true,",
      "                 'info': false,",
      "                 'lengthChange': false,",
      "                 'ordering': d[n].length > 1,",
      "                 'order': [],",
      "                 'paging': true,",
      "                 'scrollX': false,",
      "                 'scrollY': false,",
      "                 'searching': false,",
      "                 'sortClasses': false,",
      "                 'pageLength': 50,",
      "                 'rowCallback': rowCallback,",
      "                 'headerCallback': headerCallback,",
      "                 'footerCallback': footerCallback,",
      "                 'columnDefs': [",
      "                  {targets: [0, 9, 10, 11], visible: false},",
      "                  {targets: '_all', className: 'dt-center'}",
      "                 ]",
      "               });",
      "  } else {",
      "    var subtable = $(id).DataTable({",
      "            'data': d[n],",
      "            'columns': columns,",
      "            'autoWidth': true,",
      "            'deferRender': true,",
      "            'info': false,",
      "            'lengthChange': false,",
      "            'ordering': d[n].length > 1,",
      "            'order': [],",
      "            'paging': true,",
      "            'scrollX': false,",
      "            'scrollY': false,",
      "            'searching': false,",
      "            'sortClasses': false,",
      "            'pageLength': 50,",
      "            'rowCallback': rowCallback,",
      "            'headerCallback': headerCallback,",
      "            'footerCallback': footerCallback,",
      "            'columnDefs': [",
      "              {targets: [0, 9, 10, 11], visible: false},",
      "              {targets: -1, visible: false},",
      "              {targets: 0, orderable: false, className: 'details-control'},",
      "              {targets: '_all', className: 'dt-center'}",
      "             ]",
      "          }).column(0).nodes().to$().css({cursor: 'pointer'});",
      "  }",
      "  subtable.MakeCellsEditable({",
      "    onUpdate: onUpdate,",
      "    inputCss: 'my-input-class',",
      "    columns: [2, 7, 8],",
      "    confirmationButton: {",
      "      confirmCss: 'my-confirm-class',",
      "      cancelCss: 'my-cancel-class'",
      "    }",
      "  });",
      "};",
      "",
      "// display the child table on click",
      "// array to store the id's of the already created child tables",
      "var children = [];",
      "table.on('click', 'td.details-control', function(){",
      "  var tbl = $(this).closest('table'),",
      "      tblId = tbl.attr('id'),",
      "      td = $(this),",
      "      row = $(tbl).DataTable().row(td.closest('tr')),",
      "      rowIdx = row.index();",
      "  if(row.child.isShown()){",
      "    row.child.hide();",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');",
      "  } else {",
      "    var childId = tblId + '-child-' + rowIdx;",
      "// this child table has not been created yet",
      "    if(children.indexOf(childId) === -1){",
      "      children.push(childId);",
      "      row.child(format(row.data(), childId)).show();",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
      "      format_datatable(row.data(), childId, rowIdx);",
      "    }else{",
      "      row.child(true);",
      "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
      "    }",
      "  }",
      "});"
    )
    # Download button
    downloadButtonJS <-  c(
      "function(xlsx) {",
      "  var table = $('#daypartTable').find('table').DataTable();",
      "  // Letters for Excel columns.",
      "  var LETTERS = [",
      "    'A','B','C','D','E','F','G','H','I','J','K','L','M',",
      "    'N','O','P','Q','R','S','T','U','V','W','X','Y','Z'",
      "  ];",
      "  // Get sheet.",
      "  var sheet = xlsx.xl.worksheets['sheet1.xml'];",
      "  // Get a clone of the sheet data.        ",
      "  var sheetData = $('sheetData', sheet).clone();",
      "  // Clear the current sheet data for appending rows.",
      "  $('sheetData', sheet).empty();",
      "  // Row count in Excel sheet.",
      "  var rowCount = 1;",
      "  // Iterate each row in the sheet data.",
      "  $(sheetData).children().each(function (index) {",
      "    // Used for DT row() API to get child data.",
      "    var rowIndex = index - 2;", #
      "    // Don't process row if its the header row.",
      sprintf("    if (index > 1 && index < %d) {", nrow(Dat)+2), #
      "      // Get row",
      "      var row = $(this.outerHTML);",
      "      // Set the Excel row attr to the current Excel row count.",
      "      row.attr('r', rowCount);",
      "      // Iterate each cell in the row to change the row number.",
      "      row.children().each(function (index) {",
      "        var cell = $(this);",
      "        // Set each cell's row value.",
      "        var rc = cell.attr('r');",
      "        rc = rc.replace(/\d+$/, \"\") + rowCount;",
      "        cell.attr('r', rc);",
      "      });",
      "      // Get the row HTML and append to sheetData.",
      "      row = row[0].outerHTML;",
      "      $('sheetData', sheet).append(row);",
      "      rowCount++;",
      "      // Get the child data - could be any data attached to the row.",
      "      // Basically this grabd all the rows of data",
      sprintf("      var childData = table.row(':eq(' + rowIndex + ')').data()[%d];", ncol(Dat)-1),
      "      if (childData.length > 0) {",
      "        var colNames = Object.keys(childData[0]).slice(1,9);",
      "        // Prepare Excel formatted row",
      "        headerRow = '<row r=\"' + rowCount +",
      "          '\"><c t=\"inlineStr\" r=\"A' + rowCount +",
      "          '\"><is><t></t></is></c>';",
      "        for(var i = 0; i < colNames.length; i++){",
      "          headerRow = headerRow +",
      "            '<c t=\"inlineStr\" r=\"' + LETTERS[i+1] + rowCount +",
      "            '\" s=\"7\"><is><t>' + colNames[i] +", 
      "            '</t></is></c>';",
      "        }",
      "        headerRow = headerRow + '</row>';",
      "        // Append header row to sheetData.",
      "        $('sheetData', sheet).append(headerRow);",
      "        rowCount++; // Inc excelt row counter.",
      "      }",
      "      // The child data is an array of rows",
      "      for (let c = 0; c < childData.length; c++) {",
      "        // Get row data.",
      "        var child = childData[c];",
      "        // Prepare Excel formatted row",
      "        var childRow = '<row r=\"' + rowCount +",
      "          '\"><c t=\"inlineStr\" r=\"A' + rowCount +",
      "          '\"><is><t></t></is></c>';",
      "        for(let i = 0; i < colNames.length; i++){",
      "          childRow = childRow +",
      "            '<c t=\"inlineStr\" r=\"' + LETTERS[i+1] + rowCount +",
      "            '\" s=\"5\"><is><t>' + child[colNames[i]] +", 
      "            '</t></is></c>';",
      "        }",
      "        childRow = childRow + '</row>';",
      "        // Append row to sheetData.",
      "        $('sheetData', sheet).append(childRow);",
      "        rowCount++; // Inc excel row counter.",
      "      }",
      "      // Just append the header row and increment the excel row counter.",
      "    } else {",
      "      $('sheetData', sheet).append(this.outerHTML);",
      "      rowCount++;",
      "    }",
      "  });",
      "}"
    )
    # Table
    table <- DT::datatable(
      Dat,
      callback = callback_js,
      rownames = rowNames,
      escape = -colIdx-1,
      style = "bootstrap4",
      extensions = 'Buttons',
      options = list(
        dom = "Bt",
        columnDefs = list(
          list(width = '30px', targets = 0),
          list(width = '330px', targets = 1),
          list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
          list(orderable = FALSE, className = 'details-control', targets = colIdx),
          list(className = "dt-center", targets = "_all")
        ),
        buttons = list(
          list(
            extend = "excel",
            className = 'btn btn-primary glyphicon glyphicon-download-alt',
            text = " Export",
            exportOptions = list(
              orthogonal = "export",
              columns = 0:(ncol(Dat)-2)
            ),
            title = excelTitle,
            orientation = "landscape",
            customize = JS(downloadButtonJS)
          )
        ),
        lengthMenu = list(c(-1, 10, 20),
                          c("All", 10, 20))
      )
    )
    # Call the html tools deps (js & css files in this directory)
    cell_edit_dep <- htmltools::htmlDependency(
      "CellEdit", "1.0.19", 
      src = 'www/',
      script = "dataTables.cellEdit.js",
      stylesheet = "dataTables.cellEdit.css"
    )
    table$dependencies <- c(table$dependencies, list(cell_edit_dep))
    
    table %>% formatStyle(
      c(MARKET[2], 'Population', SQAD_CPP_DOLLAR, SQAD_CPM_DOLLAR, OVERRIDE_CPP_DOLLAR, OVERRIDE_CPM_DOLLAR),
      target = 'row',
      backgroundColor = "#F5F2F2"
    )
  }, server = FALSE)
  
  ### This is where I am trying to save the edits to the table and
  ### use those new values for the below table 
  output$market_costings_gross_net_table <- renderTable({
    # Get the data from reative function
    market_costings <- market_level_view()
    reactive_market_costings <- reactiveValues(data = market_costings)
    
    proxy <- dataTableProxy("daypartTable")
    
    observeEvent(input$tableRefresh, {
      DT::replaceData(proxy, reactive_market_costings$data)
    })
  })

第一轮

要在 Shiny 中获取数据:

  • footerCallback函数末尾添加这一行:

    "    Shiny.setInputValue('data:nestedData', table.data().toArray());",
    
  • 在 Shiny 应用程序之前,添加:

  library(jsonlite)

  registerInputHandler(
    "nestedData", 
    function(data, ...){
      fromJSON(toJSON(data))
    },
    force = TRUE
  )
  • 然后数据在 input[["data"]]:
  • 中可用
  observe({
    print(input[["data"]])
  })

如果您需要更多帮助,请编辑您的 post 以使您的代码可重现。我没有此代码的最新版本。


第二轮

正如我在评论中所说,最好在 render 选项中使用 toFixed。我还将可编辑单元格的类型设置为number,这样编辑单元格的值就不会转换为字符串,而且编辑框里有微调箭头。

library(DT)

df_children <-
  structure(
    list(
      Market = c(
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ABILENE-SWEETWATER",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY",
        "ALBANY-SCHENECTADY-TROY, NY"
      ),
      Daypart = c(
        "Daytime",
        "Early Fringe",
        "Early Morning",
        "Early News",
        "Late Fringe",
        "Late News",
        "Prime Access",
        "Prime Time",
        "tv_2",
        "tv_3",
        "tv_cross_screen",
        "Daytime",
        "Early Fringe",
        "Early Morning",
        "Early News",
        "Late Fringe",
        "Late News",
        "Prime Access",
        "Prime Time",
        "tv_2",
        "tv_3",
        "tv_cross_screen"
      ),
      `Mix (%)` = c(15,
                    10, 15, 10, 5, 5, 10, 10, 0, 0, 0, 15, 10, 15, 10, 5, 5, 10,
                    10, 0, 0, 0),
      `Spot:30 (%)` = c(15, 10, 15, 10, 5, 5, 10, 10,
                        0, 0, 0, 15, 10, 15, 10, 5, 5, 10, 10, 0, 0, 0),
      `Spot:15 (%)` = c(0,
                        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      `Gross CPP ($)` = c(
        18,
        18,
        16,
        23,
        24,
        40,
        26,
        44,
        0,
        0,
        0,
        77,
        71,
        61,
        78,
        109,
        145,
        93,
        213,
        0,
        0,
        0
      ),
      `Gross CPM ($)` = c(
        1.57,
        1.57,
        1.39,
        2,
        2.09,
        3.49,
        2.27,
        3.83,
        23,
        21,
        13,
        6.71,
        6.19,
        5.32,
        6.8,
        9.5,
        12.63,
        8.1,
        18.56,
        23,
        21,
        13
      ),
      `Historical Composite CPP ($)` = c(0,
                                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      `Historical Composite CPM ($)` = c(0, 0, 0, 0, 0, 0, 0, 0,
                                         0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
      population = c(
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        47200L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L,
        162700L
      ),
      slider_60s = c(
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4,
        0.4
      ),
      slider_30s = c(
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6,
        0.6
      )
    ),
    .Names = c(
      "Market",
      "Daypart",
      "Mix (%)",
      "Spot:30 (%)",
      "Spot:15 (%)",
      "Gross CPP ($)",
      "Gross CPM ($)",
      "Historical Composite CPP ($)",
      "Historical Composite CPM ($)",
      "population",
      "slider_60s",
      "slider_30s"
    ),
    class = "data.frame",
    row.names = c(NA,-22L)
  )

df_parent <-
  structure(
    list(
      Market = c("ABILENE-SWEETWATER", "ALBANY-SCHENECTADY-TROY, NY"),
      `Gross CPP` = c(1.94, 7.89),
      `Gross CPM` = c(1.02, 0.82),
      `Historical Composite Gross CPP (if applicable)` = c(0, 0),
      `Historical Composite Gross CPM (if applicable)` = c(0, 0)
    ),
    .Names = c(
      "Market",
      "Gross CPP",
      "Gross CPM",
      "Historical Composite Gross CPP (if applicable)",
      "Historical Composite Gross CPM (if applicable)"
    ),
    row.names = c(NA,-2L),
    class = "data.frame"
  )

# function to make the required dataframe
NestedData <- function(dat, children){
  stopifnot(length(children) == nrow(dat))
  g <- function(d){
    if(is.data.frame(d)){
      purrr::transpose(d)
    }else{
      purrr::transpose(NestedData(d[[1]], children = d$children))
    }
  }
  subdats <- lapply(children, g)
  oplus <- sapply(subdats, function(x) if(length(x)) "&oplus;" else "")
  cbind(" " = oplus, dat, "_details" = I(subdats), stringsAsFactors = FALSE)
}

# make the required dataframe
# one must have: length(children) == nrow(dat)
Dat <- NestedData(
  dat = df_parent,
  children = split(df_children, df_children$Market)
)

## whether to show row names (set TRUE or FALSE)
rowNames <- FALSE
colIdx <- as.integer(rowNames)

## make the callback
parentRows <- which(Dat[,1] != "")

callback_js = JS(
  "var ok = true;",
  "function onUpdate(updatedCell, updatedRow, oldValue) {",
  "  var column = updatedCell.index().column;",
  "  if(column === 8){",
  "    ok = false;",
  "  }else if(column === 7){",
  "    ok = true;",
  "  }",
  "}",
  "function render0(data, type, row) {", # @Timothy, new
  "  if(type === 'display') {",
  "    return parseFloat(data).toFixed(0);",
  "  } else {",
  "    return data;",
  "  }",
  "}",
  "function render2(data, type, row) {", # @Timothy, new
  "  if(type === 'display') {",
  "    return parseFloat(data).toFixed(2);",
  "  } else {",
  "    return data;",
  "  }",
  "}",
  sprintf("var parentRows = [%s];", toString(parentRows-1)),
  sprintf("var j0 = %d;", colIdx),
  "var nrows = table.rows().count();",
  "for(var i=0; i < nrows; ++i){",
  "  if(parentRows.indexOf(i) > -1){",
  "    table.cell(i,j0).nodes().to$().css({cursor: 'pointer'});",
  "  }else{",
  "    table.cell(i,j0).nodes().to$().removeClass('details-control');",
  "  }",
  "}",
  "",
  "// make the table header of the nested table",
  "var format = function(d, childId){",
  "  if(d != null){",
  "    var html = ",
  "      '<table class=\"display compact hover\" ' + ",
  "      'style=\"padding-left: 30px;\" id=\"' + childId + '\"><thead><tr>';",
  "    for(var key in d[d.length-1][0]){",
  "      html += '<th>' + key + '</th>';",
  "    }",
  "    html += '</tr></thead><tfoot><tr>'",
  "    for(var key in d[d.length-1][0]){",
  "      html += '<th></th>';",
  "    }",
  "    return html + '</tr></tfoot></table>';",
  "  } else {",
  "    return '';",
  "  }",
  "};",
  "",
  "// row callback to style the rows of the child tables",
  "var rowCallback = function(row, dat, displayNum, index){",
  "  if($(row).hasClass('odd')){",
  "    $(row).css('background-color', 'white');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', 'lightgreen');",
  "    }, function() {",
  "      $(this).css('background-color', 'white');",
  "    });",
  "  } else {",
  "    $(row).css('background-color', 'white');",
  "    $(row).hover(function(){",
  "      $(this).css('background-color', 'lightblue');",
  "    }, function() {",
  "      $(this).css('background-color', 'white');",
  "    });",
  "  }",
  "};",
  "",
  "// header callback to style the header of the child tables",
  "var headerCallback = function(thead, data, start, end, display){",
  "  $('th', thead).css({",
  "    'color': 'black',",
  "    'background-color': 'white'",
  "  });",
  "};",
  "",
  "// make the datatable",
  "var format_datatable = function(d, childId, rowIdx){",
  "  // footer callback to display the totals",
  "  // and update the parent row",
  "  var footerCallback = function(tfoot, data, start, end, display){", # @Timothy, I removed all the 'toFixed'
  "    $('th', tfoot).css('background-color', '#F5F2F2');",
  "    var api = this.api();",
  "// update the Override CPM when the Override CPP is changed",
  "    var col_override_cpp = api.column(7).data();",
  "    var col_population = api.column(9).data();",
  "    if(ok){",
  "      for(var i = 0; i < col_override_cpp.length; i++){",
  "        api.cell(i,8).data(((parseFloat(col_override_cpp[i])*100)/(parseFloat(col_population[i])/1000)));",
  "      }",
  "    }",
  "// update the Override CPP when the Override CPM is changed",
  "    var col_override_cpm = api.column(8).data();",
  "    for(var i = 0; i < col_override_cpm.length; i++){",
  "      api.cell(i,7).data(((parseFloat(col_override_cpm[i])*parseFloat(col_population[i])/1000)/100));",
  "    }",
  "// Update the spot mixes",
  "    var col_mix_percentage = api.column(2).data();",
  "    var col_mix60_mix30 = api.column(10).data();",
  "    var col_mix30_mix15 = api.column(11).data();",
  "    for(var i = 0; i < col_mix_percentage.length; i++){",
  "      api.cell(i,3).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix60_mix30[i])));",
  "      api.cell(i,4).data((parseFloat(col_mix_percentage[i])*parseFloat(col_mix30_mix15[i])));",
  "    }",
  "    var child_col_CPM = api.column(6).data();",
  "    for(var i = 0; i < child_col_CPM.length; i++){",
  "      api.cell(i,6).data(parseFloat(child_col_CPM[i]));",
  "    }",
  "// Make the footer sums",
  "    api.columns().eq(0).each(function(index){",
  "      if(index == 0) return $(api.column(index).footer()).html('Mix Total');",
  "      var coldata = api.column(index).data();",
  "      var total = coldata",
  "          .reduce(function(a, b){return parseFloat(a) + parseFloat(b);}, 0);",
  "      if(index == 3 || index == 4 ||index == 5 || index == 6 || index == 7 || index == 8) {",
  "        $(api.column(index).footer()).html('');",
  "      } else {",
  "        $(api.column(index).footer()).html(total);",
  "      }",
  "      if(total == 100) {",
  "        $(api.column(index).footer()).css({'color': 'green'});",
  "      } else {",
  "        $(api.column(index).footer()).css({'color': 'red'});",
  "      }",
  "    })",
  "  // update the parent row", # @Timothy, I replaced everywhere parseInt with parseFloat
  "    var col_share = api.column(2).data();",
  "    var col_CPP = api.column(5).data();",
  "    var col_CPM = api.column(6).data();",
  "    var col_Historical_CPP = api.column(7).data();",
  "    var col_Historical_CPM = api.column(8).data();",
  "    var CPP = 0, CPM = 0, Historical_CPP = 0, Historical_CPM = 0;",
  "    for(var i = 0; i < col_share.length; i++){",
  "      CPP += (parseFloat(col_share[i])*parseFloat(col_CPP[i]));",
  "      CPM += (parseFloat(col_share[i])*parseFloat(col_CPM[i]));",
  "      Historical_CPP += (parseFloat(col_share[i])*parseFloat(col_Historical_CPP[i]));",
  "      Historical_CPM += (parseFloat(col_share[i])*parseFloat(col_Historical_CPM[i]));",
  "    }",
  "    table.cell(rowIdx, j0+2).data(CPP/100);", # @Timothy, there were errors here (it's j0 + 2/3/4/5)
  "    table.cell(rowIdx, j0+3).data(CPM/100);",
  "    table.cell(rowIdx, j0+4).data(Historical_CPP/100);",
  "    table.cell(rowIdx, j0+5).data(Historical_CPM/100);",
  "    Shiny.setInputValue('data:nestedData', table.data().toArray());",
  "  }",
  "  var n = d.length - 1;",
  "  var id = 'table#' + childId;",
  "  var columns = Object.keys(d[n][0]).map(function(x){",
  "    return {data: x, title: x};",
  "  });",
  "  var subtable = $(id).DataTable({",
  "                 'data': d[n],",
  "                 'columns': columns,",
  "                 'autoWidth': true,",
  "                 'deferRender': true,",
  "                 'info': false,",
  "                 'lengthChange': false,",
  "                 'ordering': d[n].length > 1,",
  "                 'order': [],",
  "                 'paging': true,",
  "                 'scrollX': false,",
  "                 'scrollY': false,",
  "                 'searching': false,",
  "                 'sortClasses': false,",
  "                 'pageLength': 50,",
  "                 'rowCallback': rowCallback,",
  "                 'headerCallback': headerCallback,",
  "                 'footerCallback': footerCallback,",
  "                 'columnDefs': [",
  "                  {targets: [2, 3, 4], render: render0},", # @Timothy, new
  "                  {targets: [5, 6, 7, 8], render: render2},", # @Timothy, new
  "                  {targets: [0, 9, 10, 11], visible: false},",
  "                  {targets: '_all', className: 'dt-center'}",
  "                 ]",
  "               });",
  "  subtable.MakeCellsEditable({",
  "    onUpdate: onUpdate,",
  "    inputCss: 'my-input-class',",
  "    columns: [2, 7, 8],", 
  "    inputTypes: [", # @Timothy, new 
  "      {column: 2, type: 'number'},",
  "      {column: 7, type: 'number'},",
  "      {column: 8, type: 'number'}",
  "    ],",
  "    confirmationButton: {",
  "      confirmCss: 'my-confirm-class',",
  "      cancelCss: 'my-cancel-class'",
  "    }",
  "  });",
  "};",
  "",
  "// display the child table on click",
  "// array to store the id's of the already created child tables",
  "var children = [];",
  "table.on('click', 'td.details-control', function(){",
  "  var tbl = $(this).closest('table'),",
  "      tblId = tbl.attr('id'),",
  "      td = $(this),",
  "      row = $(tbl).DataTable().row(td.closest('tr')),",
  "      rowIdx = row.index();",
  "  if(row.child.isShown()){",
  "    row.child.hide();",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');",
  "  } else {",
  "    var childId = tblId + '-child-' + rowIdx;",
  "// this child table has not been created yet",
  "    if(children.indexOf(childId) === -1){",
  "      children.push(childId);",
  "      row.child(format(row.data(), childId)).show();",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
  "      format_datatable(row.data(), childId, rowIdx);",
  "    }else{",
  "      row.child(true);",
  "    td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');",
  "    }",
  "  }",
  "});"
)


render_js <- JS( # @Timothy, new
  "function(data, type, row) {",
  "  if(type === 'display') {",
  "    return '$' + data.toFixed(2);",
  "  } else {",
  "    return data;",
  "  }",
  "}"
)


## the datatable
dtable <- datatable(
  Dat, callback = callback_js, rownames = rowNames, escape = -colIdx-1, 
  extensions = "Buttons", 
  options = list(
    dom = "Bfrtip",
    columnDefs = list(
      list(render = render_js, targets = colIdx + 1 + 1:4), # @Timothy, new
      list(visible = FALSE, targets = ncol(Dat)-1+colIdx),
      list(orderable = FALSE, className = 'details-control', targets = colIdx),
      list(className = "dt-center", targets = "_all")
    )
  )
)

path <- "~/Work/R/DT" # folder containing the files dataTables.cellEdit.js
                      # and dataTables.cellEdit.css
dep <- htmltools::htmlDependency(
  "CellEdit", "1.0.19", path, 
  script = "dataTables.cellEdit.js", stylesheet = "dataTables.cellEdit.css")
dtable$dependencies <- c(dtable$dependencies, list(dep))






library(shiny)
library(jsonlite)

registerInputHandler(
  "nestedData", 
  function(data, ...){
    fromJSON(toJSON(data))
  },
  force = TRUE
)

ui <- fluidPage(
  br(),
  DTOutput("dtable")
)

server <- function(input, output){
  
  output[["dtable"]] <- renderDT(dtable)
  
  observe({
    print(input[["data"]])
  })
  
}

shinyApp(ui, server)