Custom Field List with frozen columns and measures

Custom UI controls and views

by Flexmonster Pivot Table

HTML

<script src="https://cdn.flexmonster.com/flexmonster.js"></script>

<div>
  <h2>Creating a custom Field List</h2>
  <p>
    This example shows how to create a custom Field List where the user can edit
    only the <b>Rows</b> and <b>Report Filters</b> boxes, while
    <b>Columns</b> and <b>Measures</b> remain the same.
  </p>
  <p>
    For more details, refer to our
    <a
      href="https://www.flexmonster.com/doc/creating-a-custom-ui-control-or-view/#creating-custom-field-list"
      target="_blank"
    >step-by-step tutorial</a>.
  </p>
</div>

<div id="wrapper">
  <div id="backdrop"></div>
  <div id="field-list" class="hideList">
    <h3>Custom Field List</h3>
    <button
      id="field-list-cancel-button"
      class="button"
      onclick="closeFieldList()"
    >
      Cancel
    </button>
    <button id="field-list-apply-button" class="button" onclick="applySlice()">
      Apply
    </button>
    <div class="field-list-layout">
      <div id="available-fields" class="field-box">
        <h3>Available fields</h3>
      </div>

      <div class="right-column">
        <div id="rows" class="field-box">
          <h3>Rows</h3>
        </div>
        <div id="report-filters" class="field-box">
          <h3>Report filters</h3>
        </div>
      </div>
    </div>
  </div>

  <div id="pivot-container"></div>
</div>

CSS

@import url(https://cdn.flexmonster.com/assets/jsfiddle-styles.css);

.field-list-layout {
  display: flex;
  gap: 30px;
  height: 90%;
  margin-top: 20px;
}

#fm-pivot-view .fm-fields-view-wrap.fm-fields-opened,
#fm-pivot-view div.fm-ui-modal-overlay {
  display: none !important;
}

#wrapper {
  font-family: system-ui;
  height: 600px;
  position: relative;
}

#available-fields {
  flex: 1;
  min-width: 150px;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  font-family: system-ui;
  font-size: 13px;
}

.right-column {
  flex: 2;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 20px;
}

#rows,
#report-filters {
  flex: 1;
  overflow-y: auto;
}

.field-box {
  font-family: sans-serif;
  border: 1px solid #ccc;
  padding: 16px;
  background-color: #fafafa;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
  display: flex;
  flex-direction: column;
}

.field-box h3 {
  margin: 0 0 10px 0;
  font-size: 15px;
  font-weight: 600;
}

.draggable-field {
  padding: 8px 12px;
  background-color: #eaeaea;
  border: 1px solid #bbb;
  border-radius: 4px;
  margin-bottom: 8px;
  margin-top: 5px;
  cursor: grab;
  transition: background-color 0.2s;
}

.draggable-field:hover {
  background-color: #dcdcdc;
}

.button {
  border-collapse: collapse;
  box-sizing: border-box;
  text-transform: uppercase;
  position: relative;
  border-radius: 4px;
  cursor: pointer;
  text-align: center;
  display: inline;
  font-size: 14px;
  font-weight: bold;
  padding: 7px 10px;
  letter-spacing: 0.5px;
  min-width: 50px;
  border: 1px solid #555;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
  height: 30px;
}

#field-list-apply-button {
  float: right;
  color: #fff;
  background: #555;
}

#field-list-cancel-button {
  float: right;
  color: #555;
  background:...

JavaScript

const pivot = new Flexmonster({
  container: "pivot-container",
  componentFolder: "https://cdn.flexmonster.com/",
  report: {
    dataSource: {
      filename: "data/data.csv",
    },
    slice: {
      rows: [],
      columns: [
        {
          uniqueName: "Category",
        },
      ],
      measures: [
        {
          uniqueName: "Price",
          aggregation: "sum",
        },
      ],
      reportFilters: [],
    },
  },
})

const backdrop = document.getElementById("backdrop")
let fieldListWasOpenedOnce = false

pivot.on("fieldslistopen", () => {
  pivot.closeFieldsList()
  openFieldList()
})

pivot.on("reportcomplete", function () {
  if (!fieldListWasOpenedOnce) {
    openFieldList()
    fieldListWasOpenedOnce = true
  }
})

function getAvailableFields() {
  const allFields = pivot.getAllHierarchies()
  const columns = pivot.getColumns().map((col) => col.uniqueName)
  const measures = pivot.getMeasures().map((measure) => measure.uniqueName)
  const rows = pivot.getRows().map((row) => row.uniqueName)
  const reportFilters = pivot.getReportFilters().map((f) => f.uniqueName)

  return allFields.filter(
    (field) =>
      !columns.includes(field.uniqueName) &&
      !measures.includes(field.uniqueName) &&
      !rows.includes(field.uniqueName) &&
      !reportFilters.includes(field.uniqueName),
  )
}

function openFieldList() {
  const availableFields = getAvailableFields()
  const currentRows = pivot.getRows()
  const currentFilters = pivot.getReportFilters()

  const renderFields = (containerId, fields) => {
    const container = document.getElementById(containerId)

    const header = container.querySelector("h3")?.outerHTML || ""
    container.innerHTML = header

    fields.forEach((field) => {
      const item = document.createElement("div")
      item.className = "draggable-field"
      item.draggable = true
      item.innerText = field.caption
     ...