Hey,
I was writing a script to display a list and choose some items from it, like the Choose from List action in Shortcuts. While testing it, the Scriptable app crashes… I don’t know why and I can’t debug it then…
Script
/**
Shows a list of items to pick from
@param {UITableCell[][]|object[]|object[][]|string[]} items
Either ["Item 1", "Item 2"]
or [[UITableCell, UITableCell, ...], [UITableCell, UITableCell, ...]]
or [{rowHeight: 20, cells: [UITableCell, <cellObject>]}, {rowHeight: 20, text: "Item 2"}]
or [[{widthWeight: 20, text: "Item", subtitle: "We don't need that subtitle", aligned: "left"}, {widthWeight: 80, image: Image, aligned: "right"}]] (these are called <cellObject>)
or a mixture of these
@param {object} [options]
Some more options
@param {string} [options.message]
The message you want to show at the top of the list
@param {number} [options.max=0]
Specify the maximum number of selectable items. 0 or not specified means that all items are selectable
@param {Boolean|Boolean[]} [options.initialSelection=false]
If it is a Boolean: true = all selected, false = none selected
If it is an array of Booleans: each item at each index is selected if the value at that index is true
@param {Boolean} [options.repeatInitialSelection=false]
If `options.initialSelection` is an array, this specifies if the array should be repeated if it is shorter than there are items
@param {Boolean} [options.showSeparators=false]
Whether or not to show seperators between items
@param {string} [options.returns="filter"]
"filter" to return an array where the item at each index is selected if the value at that index in the array is true
"!filter" to return the inverted array of "filter"
"selected" to return the selected items
"!selected" to return the not selected items
"index" to return the index of the selected items
"!index" to return the index of the not selected items
*/
async function choose(items, options) {
function isCellObj(i) {
return i instanceof UITableCell || (typeof i === "object" && (i.text || i.subtitle || i.image instanceof Image));
}
if (!items || !Array.isArray(items) || !items.length) {
return Promise.reject("Please pass at least one item in the array to show");
}
if (
!items.every(i =>
typeof i === "string" ||
(Array.isArray(i) && i.every(isCellObj)) ||
(typeof i === "object" && Array.isArray(i.cells) && i.cells.every(isCellObj)) ||
(typeof i === "object" && typeof i.text === "string")
)
) {
return Promise.reject("The items have to be either an array of strings, an array of arrays of UITableCells, an array of objects with the key \"cells\" that holds an array of UITableCells or objects with at least one of the keys \"text\", \"subtitle\" or \"image\", an array of objects with the key \"text\" that holds a string or an array of arrays of objects with at least one of the keys \"text\", \"subtitle\" or \"image\"");
}
let ui = new UITable();
const checkmarkWidth = 8;
let selectionOrder = [];
opions = options || {};
options.max = options.max || 0;
options.initialSelection = options.initialSelection || false;
options.repeatInitialSelection = options.repeatInitialSelection || false;
options.showSeparators = options.showSeparators || false;
if (!Array.isArray(options.initialSelection)) {
options.repeatInitialSelection = true;
options.initialSelection = [options.initialSelection];
}
options.initialSelection = options.initialSelection.map(s => !!s);
ui.showSeparators = options.showSeparators;
let selected = options.initialSelection;
if (selected.length < items.length) {
selected.length = items.length;
for (let i = options.initialSelection.length; i < selected.length; i++) {
selected[i] = options.initialSelection[i % options.initialSelection.length];
}
} else if (seleted.length > items.length) {
selected = selected.slice(0, items.length);
}
function createRows() {
ui.removeAllRows();
let row, cell;
if (selected.filter(s => s).length > options.max) {
let sum = 0;
if (selectionOrder.length) {
let i = selectionOrder.shift();
selected[i] = false;
} else {
selected = selected.map((s, i) => (s && ++sum <= options.max) ? s : false);
}
}
if (options.message) {
row = new UITableRow();
row.dismissOnSelect = false;
row.addText(options.message);
// TODO: measure text height. Maybe with WebView?
row.height = 60;
ui.addRow(row);
}
// selection things & cancel button
row = new UITableRow();
row.dismissOnSelect = false;
// invert selection
cell = row.addButton("\u25e9 \u2192 \u25ea");
cell.onTap = () => {
selected = selected.map(s => !s);
selectionOrder = [];
createRows();
};
// select all
cell = row.addButton("\u2714\u0336");
cell.onTap = () => {
selected = selected.map(s => false);
selectionOrder = [];
createRows();
};
// cancel
cell = row.addButton("Cancel");
cell.dismissOnTap = true;
cell.onTap = () => {
selected = undefined;
};
items.forEach((item, i) => {
row = new UITableRow();
row.dismissOnSelect = false;
row.onSelect = (number) => {
selected[i] = !selected[i];
selectionOrder = selectionOrder.filter(s => s !== i);
if (selected[i]) {
selectionOrder.push(i);
}
createRows();
}
// checkmark
let checkmarkCell = UITableCell.text(selected[i] ? "\u2611" : "\u2610");
checkmarkCell.centerAligned();
row.addCell(checkmarkCell);
totalWidth = [];
if (typeof item === "object" && !Array.isArray(item)) {
if (item.height) row.height = item.height;
if (item.text) item = text;
else if (Array.isArray(item.cells)) item = item.cells;
}
if (typeof item === "string") {
cell = row.addText(item);
cell.widthWeight = 100;
totalWidth.push(cell.widthWeight);
} else {
item.forEach(c => {
if (c instanceof UITableCell) {
row.addCell(c);
totalWidth.push(c);
} else if (c.image) {
cell = row.addImage(c.image);
c.widthWeight && (cell.widthWeight = c.widthWeight);
c.align && cell[c.align + "Aligned"]();
totalWidth.push(cell);
} else {
cell = row.addText(c.text, c.subtitle);
c.widthWeight && (cell.widthWeight = c.widthWeight);
c.align && cell[c.align + "Aligned"]();
totalWidth.push(cell);
}
});
}
// calculate the average width of all cells if there are cells with default width 0
// if all cells have the default width 0, calculate the average width of 100 - checkmarkWidth
let sum = totalWidth.reduce((acc, cur) => acc + cur.widthWeight);
let avg = sum / totalWidth.filter(i => i.widthWeight).length;
if (sum === 0) {
sum = 100;
avg = sum / totalWidth.length;
} else {
sum += totalWidth.filter(i => !i.widthWeight).length * avg;
}
totalWidth.forEach(i => {
if (!i.widthWeight)
i.widthWeight = avg;
});
// calculate weight value of checkmarkWidth as pixels
let deviceWidth = Device.screenSize().width * Device.screenScale()
checkmarkCell.widthWeight = sum / (deviceWidth - checkmarkWidth) * checkmarkWidth;
ui.addRow(row);
});
ui.reload();
}
createRows();
await ui.present();
if (selected == null) return selected;
let invert = options.returns && options.returns[0] === "!";
if (invert) {
options.returns = options.returns.substr(1);
selected = selected.map(s => !s);
}
switch (options.returns) {
case "filter":
default:
break;
case "index":
selected = selected.map((s, i) => s && i).filter(i => i !== false);
break;
case "selected":
selected = items.filter((item, i) => selected[i])
}
}
let res = await choose(["a","b","c","d","e"], {
message: "This should be short"
})
log(res)
@simonbs Can you please look into this?