blob: 451b092c0e0ebda6de0fcc6f24ec28c944c56999 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Listener, Phase} from "./tobago-listener";
import {DomUtils} from "./tobago-utils";
import {CommandHelper} from "./tobago-command";
import {Page} from "./tobago-page";
class Sheet {
static readonly SHEETS: Map<string, Sheet> = new Map<string, Sheet>();
static readonly SCROLL_BAR_SIZE: number = Sheet.getScrollBarSize();
id: string;
clickActionId: string;
clickReloadComponentId: string;
dblClickActionId: string;
dblClickReloadComponentId: string;
mousemoveData: any;
mousedownOnRowData: any;
static init(element: HTMLElement) {
console.time("[tobago-sheet] init");
for (const sheetElement of DomUtils.selfOrElementsByClassName(element, "tobago-sheet")) {
const sheet = new Sheet(sheetElement);
Sheet.SHEETS.set(sheet.id, sheet);
}
console.timeEnd("[tobago-sheet] init");
}
private static getScrollBarSize() {
const body = document.getElementsByTagName("body").item(0);
const outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.overflow = "scroll";
body.append(outer);
const inner = document.createElement("div");
inner.style.width = "100%";
outer.append(inner);
const widthWithScroll = inner.offsetWidth;
body.removeChild(outer);
return 100 - widthWithScroll;
}
private static isInputElement(element: HTMLElement) {
return ["INPUT", "TEXTAREA", "SELECT", "A", "BUTTON"].indexOf(element.tagName) > -1;
}
constructor(element: HTMLElement) {
this.id = element.id;
const commands = element.dataset["tobagoRowAction"];
const behavior = element.dataset["tobagoBehaviorCommands"];
this.clickActionId = null;//todo commands.click.action;
this.clickReloadComponentId = null;//todo commands.click.partially; // fixme: partially no longer used?
this.dblClickActionId = null;//todo commands.dblclick.action;
this.dblClickReloadComponentId = null;//todo commands.dblclick.partially;// fixme: partially no longer used?
// synchronize column widths ----------------------------------------------------------------------------------- //
// basic idea: there are two possible sources for the sizes:
// 1. the columns attribute of <tc:sheet> like {"columns":[1.0,1.0,1.0]}, held by data attribute "tobago-layout"
// 2. the hidden field which may contain a value like ",300,200,100,"
//
// The 1st source usually is the default set by the developer.
// The 2nd source usually is the value set by the user manipulating the column widths.
//
// So, if the 2nd is set, we use it, if not set, we use the 1st source.
let columnWidths = this.loadColumnWidths();
console.info("columnWidths: " + columnWidths);
if (columnWidths && columnWidths.length === 0) { // active, but empty
// otherwise use the layout definition
const tokens = JSON.parse(element.dataset["tobagoLayout"]).columns;
const columnRendered = this.isColumnRendered();
const headerCols = this.getHeaderCols();
const bodyTable = this.getBodyTable();
const bodyCols = this.getBodyCols();
console.assert(headerCols.length - 1 === bodyCols.length, "header and body column number doesn't match");
let sumRelative = 0; // tbd: is this needed?
let widthRelative = bodyTable.offsetWidth;
for (let i = 0; i < tokens.length; i++) {
if (columnRendered[i]) {
if (typeof tokens[i] === "number") {
sumRelative += tokens[i];
} else if (typeof tokens[i] === "object" && tokens[i].measure !== undefined) {
const intValue = parseInt(tokens[i].measure);
if (tokens[i].measure.lastIndexOf("px") > 0) {
widthRelative -= intValue;
} else if (tokens[i].measure.lastIndexOf("%") > 0) {
widthRelative -= bodyTable.offsetWidth * intValue / 100;
}
} else {
console.debug("auto? = " + tokens[i]);
}
}
}
if (widthRelative < 0) {
widthRelative = 0;
}
let headerBodyColCount = 0;
for (let i = 0; i < tokens.length; i++) {
let colWidth = 0;
if (columnRendered[i]) {
if (typeof tokens[i] === "number") {
colWidth = tokens[i] * widthRelative / sumRelative;
} else if (typeof tokens[i] === "object" && tokens[i].measure !== undefined) {
const intValue = parseInt(tokens[i].measure);
if (tokens[i].measure.lastIndexOf("px") > 0) {
colWidth = intValue;
} else if (tokens[i].measure.lastIndexOf("%") > 0) {
colWidth = bodyTable.offsetWidth * intValue / 100;
}
} else {
console.debug("auto? = " + tokens[i]);
}
if (colWidth > 0) { // because tokens[i] == "auto"
headerCols.item(headerBodyColCount).setAttribute("width", String(colWidth));
bodyCols.item(headerBodyColCount).setAttribute("width", String(colWidth));
}
headerBodyColCount++;
}
}
}
this.addHeaderFillerWidth();
// resize column: mouse events -------------------------------------------------------------------------------- //
for (const resizeElement of <NodeListOf<HTMLElement>>element.querySelectorAll(".tobago-sheet-headerResize")) {
resizeElement.addEventListener("click", function () {
return false;
});
resizeElement.addEventListener("mousedown", this.mousedown.bind(this));
}
// scrolling -------------------------------------------------------------------------------------------------- //
const sheetBody = this.getBody();
// restore scroll position
const value: number[] = JSON.parse(this.getHiddenScrollPosition().getAttribute("value"));
sheetBody.scrollLeft = value[0];
sheetBody.scrollTop = value[1];
this.syncScrolling();
// scroll events
sheetBody.addEventListener("scroll", this.scroll.bind(this));
// add selection listeners ------------------------------------------------------------------------------------ //
const selectionMode = element.dataset["tobagoSelectionMode"];
if (selectionMode === "single" || selectionMode === "singleOrNone" || selectionMode === "multi") {
for (const row of this.getRows()) {
row.addEventListener("mousedown", this.mousedownOnRow.bind(this));
row.addEventListener("click", this.clickOnRow.bind(this));
// todo: check if this works correctly
const sheet = Sheet.SHEETS.get(this.id);
if (sheet && sheet.dblClickActionId) {
row.addEventListener("dblclick", function (event) {
// todo: re-implement
sheet.doDblClick(event);
});
}
}
}
for (const checkbox of <NodeListOf<HTMLInputElement>>element.querySelectorAll(
".tobago-sheet-cell > input.tobago-sheet-columnSelector")) {
checkbox.addEventListener("click", (event) => {
event.preventDefault();
});
}
// ---------------------------------------------------------------------------------------- //
for (const checkbox of <NodeListOf<HTMLInputElement>>element.querySelectorAll(
".tobago-sheet-header .tobago-sheet-columnSelector")) {
checkbox.addEventListener("click", this.clickOnCheckbox.bind(this));
}
// init paging by pages ---------------------------------------------------------------------------------------- //
for (const pagingText of <NodeListOf<HTMLElement>>element.querySelectorAll(".tobago-sheet-pagingText")) {
pagingText.addEventListener("click", this.clickOnPaging.bind(this));
const pagingInput = pagingText.querySelector("input.tobago-sheet-pagingInput");
pagingInput.addEventListener("blur", this.blurPaging.bind(this));
pagingInput.addEventListener("keydown", function (event: KeyboardEvent) {
if (event.keyCode === 13) {
event.stopPropagation();
event.preventDefault();
event.currentTarget.dispatchEvent(new Event("blur"));
}
});
}
}
loadColumnWidths(): number[] {
const hidden = document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "widths");
if (hidden) {
return JSON.parse(hidden.getAttribute("value"));
} else {
return undefined;
}
}
saveColumnWidths(widths: number[]) {
const hidden = document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "widths");
if (hidden) {
hidden.setAttribute("value", JSON.stringify(widths));
} else {
console.warn("ignored, should not be called, id='" + this.id + "'");
}
}
getElement(): HTMLElement {
return document.getElementById(this.id);
}
isColumnRendered(): boolean[] {
const hidden = document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "rendered");
return JSON.parse(hidden.getAttribute("value"));
}
addHeaderFillerWidth() {
const last = document.getElementById(this.id).querySelector(".tobago-sheet-headerTable col:last-child");
if (last) {
last.setAttribute("width", String(Sheet.SCROLL_BAR_SIZE));
}
}
mousedown(event: MouseEvent) {
Page.page().dataset["SheetMousedownData"] = this.id;
// begin resizing
console.debug("down");
const resizeElement = event.currentTarget as HTMLElement;
const columnIndex = parseInt(resizeElement.dataset["tobagoColumnIndex"]);
const headerColumn = this.getHeaderCols().item(columnIndex);
const mousemoveListener = this.mousemove.bind(this);
const mouseupListener = this.mouseup.bind(this);
this.mousemoveData = {
columnIndex: columnIndex,
originalClientX: event.clientX,
originalHeaderColumnWidth: parseInt(headerColumn.getAttribute("width")),
mousemoveListener: mousemoveListener,
mouseupListener: mouseupListener
};
document.addEventListener("mousemove", mousemoveListener);
document.addEventListener("mouseup", mouseupListener);
}
mousemove(event: MouseEvent) {
console.debug("move");
let delta = event.clientX - this.mousemoveData.originalClientX;
delta = -Math.min(-delta, this.mousemoveData.originalHeaderColumnWidth - 10);
let columnWidth = this.mousemoveData.originalHeaderColumnWidth + delta;
this.getHeaderCols().item(this.mousemoveData.columnIndex).setAttribute("width", columnWidth);
this.getBodyCols().item(this.mousemoveData.columnIndex).setAttribute("width", columnWidth);
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
return false;
}
mouseup(event: MouseEvent) {
console.debug("up");
// switch off the mouse move listener
document.removeEventListener("mousemove", this.mousemoveData.mousemoveListener);
document.removeEventListener("mouseup", this.mousemoveData.mouseupListener);
// copy the width values from the header to the body, (and build a list of it)
const tokens = JSON.parse(this.getElement().dataset["tobagoLayout"]).columns;
const columnRendered = this.isColumnRendered();
const columnWidths = this.loadColumnWidths();
const bodyTable = this.getBodyTable();
const headerCols = this.getHeaderCols();
const bodyCols = this.getBodyCols();
const widths: number[] = [];
let usedWidth = 0;
let headerBodyColCount = 0;
for (let i = 0; i < columnRendered.length; i++) {
if (columnRendered[i]) {
// last column is the filler column
const newWidth = parseInt(headerCols.item(headerBodyColCount).getAttribute("width"));
// for the hidden field
widths[i] = newWidth;
usedWidth += newWidth;
const oldWidth = parseInt(bodyCols.item(headerBodyColCount).getAttribute("width"));
if (oldWidth !== newWidth) {
bodyCols.item(headerBodyColCount).setAttribute("width", String(newWidth));
}
headerBodyColCount++;
} else if (columnWidths !== undefined && columnWidths.length >= i) {
widths[i] = columnWidths[i];
} else {
if (typeof tokens[i] === "number") {
widths[i] = 100;
} else if (typeof tokens[i] === "object" && tokens[i].measure !== undefined) {
const intValue = parseInt(tokens[i].measure);
if (tokens[i].measure.lastIndexOf("px") > 0) {
widths[i] = intValue;
} else if (tokens[i].measure.lastIndexOf("%") > 0) {
widths[i] = parseInt(bodyTable.style.width) / 100 * intValue;
}
}
}
}
// store the width values in a hidden field
this.saveColumnWidths(widths);
return false;
}
scroll(event) {
console.debug("scroll");
const sheetBody: HTMLElement = event.currentTarget;
this.syncScrolling();
// store the position in a hidden field
const hidden = this.getHiddenScrollPosition();
hidden.setAttribute("value",
JSON.stringify([Math.round(sheetBody.scrollLeft), Math.round(sheetBody.scrollTop)]));
}
mousedownOnRow(event: MouseEvent) {
console.debug("mousedownOnRow");
this.mousedownOnRowData = {
x: event.clientX,
y: event.clientY
};
}
clickOnCheckbox(event: MouseEvent) {
const checkbox = event.currentTarget as HTMLInputElement;
if (checkbox.checked) {
this.selectAll();
} else {
this.deselectAll();
}
}
clickOnRow(event: MouseEvent) {
const row = event.currentTarget as HTMLTableRowElement;
if (row.classList.contains("tobago-sheet-columnSelector") || !Sheet.isInputElement(row)) {
const sheet = this.getElement();
if (Math.abs(this.mousedownOnRowData.x - event.clientX)
+ Math.abs(this.mousedownOnRowData.y - event.clientY) > 5) {
// The user has moved the mouse. We assume, the user want to select some text inside the sheet,
// so we doesn't select the row.
return;
}
if (window.getSelection) {
window.getSelection().removeAllRanges();
}
const rows = this.getRows();
const selector = this.getSelectorCheckbox(row);
const selectionMode = this.getElement().dataset["tobagoSelectionMode"];
if ((!event.ctrlKey && !event.metaKey && !selector)
|| selectionMode === "single" || selectionMode === "singleOrNone") {
this.deselectAll();
this.resetSelected();
}
const lastClickedRowIndex = parseInt(sheet.dataset["tobagoLastClickedRowIndex"]);
if (event.shiftKey && selectionMode === "multi" && lastClickedRowIndex > -1) {
if (lastClickedRowIndex <= row.sectionRowIndex) {
this.selectRange(rows, lastClickedRowIndex, row.sectionRowIndex, true, false);
} else {
this.selectRange(rows, row.sectionRowIndex, lastClickedRowIndex, true, false);
}
} else if (selectionMode !== "singleOrNone" || !this.isRowSelected(row)) {
this.toggleSelection(row, selector);
}
const rowAction = sheet.dataset["tobagoRowAction"];
const commands = rowAction ? JSON.parse(rowAction) : undefined;
const click = commands ? commands.click : undefined;
const clickActionId = click ? click.action : undefined;
const clickExecuteIds = click ? click.execute : undefined;
const clickRenderIds = click ? click.render : undefined;
if (clickActionId) {
let action: string;
const index = clickActionId.indexOf(sheet.id);
const rowIndex = this.getDataIndex(row);
if (index >= 0) {
action = sheet.id + ":" + rowIndex + ":" + clickActionId.substring(index + sheet.id.length + 1);
} else {
action = sheet.id + ":" + rowIndex + ":" + clickActionId;
}
if (clickExecuteIds && clickExecuteIds.length > 0) {
jsf.ajax.request(
action,
event,
{
//"javax.faces.behavior.event": "click",
execute: clickExecuteIds,
render: clickRenderIds
});
} else {
CommandHelper.submitAction(row, action);
}
}
}
}
clickOnPaging(event: MouseEvent) {
const element = event.currentTarget as HTMLElement;
const output = element.querySelector(".tobago-sheet-pagingOutput") as HTMLElement;
output.style.display = "none";
const input = element.querySelector(".tobago-sheet-pagingInput") as HTMLInputElement;
input.style.display = "initial";
input.focus();
input.select();
}
blurPaging(event: FocusEvent) {
const input = event.currentTarget as HTMLInputElement;
const output = input.parentElement.querySelector(".tobago-sheet-pagingOutput") as HTMLElement;
if (output.innerHTML !== input.value) {
console.debug(
"Reloading sheet '" + this.id + "' old value='" + output.innerHTML + "' new value='" + input.value + "'");
output.innerHTML = input.value;
jsf.ajax.request(
input.id,
null,
{
"javax.faces.behavior.event": "reload",
execute: this.id,
render: this.id
});
} else {
console.info("no update needed");
input.style.display = "none";
output.style.display = "initial";
}
}
syncScrolling() {
// sync scrolling of body to header
const header = this.getHeader();
if (header) {
header.scrollLeft = this.getBody().scrollLeft;
}
}
getHeader(): HTMLElement {
return this.getElement().querySelector(".tobago-sheet>header");
}
getHeaderTable(): HTMLElement {
return this.getElement().querySelector(".tobago-sheet>header>table");
}
getHeaderCols(): NodeListOf<HTMLElement> {
return this.getElement().querySelectorAll(".tobago-sheet>header>table>colgroup>col");
}
getBody(): HTMLElement {
return this.getElement().querySelector(".tobago-sheet>.tobago-sheet-body");
}
getBodyTable(): HTMLElement {
return this.getElement().querySelector(".tobago-sheet>.tobago-sheet-body>.tobago-sheet-bodyTable");
}
getBodyCols(): NodeListOf<HTMLElement> {
return this.getElement().querySelectorAll(".tobago-sheet>.tobago-sheet-body>.tobago-sheet-bodyTable>colgroup>col");
}
getHiddenSelected(): HTMLInputElement {
return document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "selected") as HTMLInputElement;
}
getHiddenScrollPosition() {
return document.getElementById(this.id + DomUtils.SUB_COMPONENT_SEP + "scrollPosition");
}
doDblClick(event) {
const row = event.currentTarget as HTMLTableRowElement;
const rowIndex = row.sectionRowIndex + this.getFirst();
if (this.dblClickActionId) {
let action;
const index = this.dblClickActionId.indexOf(this.id);
if (index >= 0) {
action = this.id + ":" + rowIndex + ":" + this.dblClickActionId.substring(index + this.id.length + 1);
} else {
action = this.id + ":" + rowIndex + ":" + this.dblClickActionId;
}
if (this.dblClickReloadComponentId && this.dblClickReloadComponentId.length > 0) {
jsf.ajax.request(
action,
event,
{
//"javax.faces.behavior.event": "dblclick",
execute: this.dblClickReloadComponentId,
render: this.dblClickReloadComponentId
});
} else {
CommandHelper.submitAction(row, action);
}
}
}
/**
* Get the element, which indicates the selection
*/
getSelectorCheckbox(row): HTMLInputElement {
return row.querySelector("tr>td>input.tobago-sheet-columnSelector");
}
getRows(): NodeListOf<HTMLTableRowElement> {
return this.getBodyTable().querySelectorAll("tbody>tr");
}
getFirst(): number {
return parseInt(this.getElement().dataset["tobagoFirst"]);
}
isRowSelected(row: HTMLTableRowElement) {
let rowIndex = +row.dataset["tobagoRowIndex"];
if (!rowIndex) {
rowIndex = row.sectionRowIndex + this.getFirst();
}
return this.isSelected(rowIndex);
}
isSelected(rowIndex: number) {
const value = <number[]>JSON.parse(this.getHiddenSelected().value);
return value.indexOf(rowIndex) > -1;
}
resetSelected() {
this.getHiddenSelected().value = JSON.stringify([]);
}
toggleSelection(row: HTMLTableRowElement, checkbox: HTMLInputElement) {
this.getElement().dataset["tobagoLastClickedRowIndex"] = String(row.sectionRowIndex);
if (checkbox && !checkbox.disabled) {
const selected = this.getHiddenSelected();
const rowIndex = this.getDataIndex(row);
if (this.isSelected(rowIndex)) {
this.deselectRow(selected, rowIndex, row, checkbox);
} else {
this.selectRow(selected, rowIndex, row, checkbox);
}
}
}
selectAll() {
const rows = this.getRows();
this.selectRange(rows, 0, rows.length - 1, true, false);
}
deselectAll() {
const rows = this.getRows();
this.selectRange(rows, 0, rows.length - 1, false, true);
}
toggleAll() {
const rows = this.getRows();
this.selectRange(rows, 0, rows.length - 1, true, true);
}
selectRange(
rows: NodeListOf<HTMLTableRowElement>, first: number, last: number, selectDeselected: boolean,
deselectSelected: boolean) {
const selected = this.getHiddenSelected();
const value = new Set<number>(JSON.parse(selected.value));
for (let i = first; i <= last; i++) {
const row = rows.item(i);
const checkbox = this.getSelectorCheckbox(row);
if (checkbox && !checkbox.disabled) {
const rowIndex = this.getDataIndex(row);
const on = value.has(rowIndex);
if (selectDeselected && !on) {
this.selectRow(selected, rowIndex, row, checkbox);
} else if (deselectSelected && on) {
this.deselectRow(selected, rowIndex, row, checkbox);
}
}
}
}
getDataIndex(row: HTMLTableRowElement): number {
const rowIndex = parseInt(row.dataset["tobagoRowIndex"]);
if (rowIndex) {
return rowIndex;
} else {
return row.sectionRowIndex + this.getFirst();
}
}
/**
* @param selected input-element type=hidden: Hidden field with the selection state information
* @param rowIndex int: zero based index of the row.
* @param row tr-element: the row.
* @param checkbox input-element: selector in the row.
*/
selectRow(selected: HTMLInputElement, rowIndex: number, row: HTMLTableRowElement, checkbox: HTMLInputElement) {
const selectedSet = new Set<number>(JSON.parse(selected.value));
selected.value = JSON.stringify(Array.from(selectedSet.add(rowIndex)));
row.classList.add("tobago-sheet-row-markup-selected");
row.classList.add("table-info");
checkbox.checked = true;
setTimeout(function () {
checkbox.checked = true;
}, 0);
}
/**
* @param selected input-element type=hidden: Hidden field with the selection state information
* @param rowIndex int: zero based index of the row.
* @param row tr-element: the row.
* @param checkbox input-element: selector in the row.
*/
deselectRow(selected: HTMLInputElement, rowIndex: number, row: HTMLTableRowElement, checkbox: HTMLInputElement) {
const selectedSet = new Set<number>(JSON.parse(selected.value));
selectedSet.delete(rowIndex);
selected.value = JSON.stringify(Array.from(selectedSet));
row.classList.remove("tobago-sheet-row-markup-selected");
row.classList.remove("table-info");
checkbox.checked = false;
// XXX check if this is still needed... Async because of TOBAGO-1312
setTimeout(function () {
checkbox.checked = false;
}, 0);
}
}
Listener.register(Sheet.init, Phase.DOCUMENT_READY);
Listener.register(Sheet.init, Phase.AFTER_UPDATE);