How to make a range in Numbers (iWork) using JXA - macos

I'm using JXA for automating a process using Numbers app. What I need is to select a range of cells to apply width, but JXA doesn't allow me to get them.
According to apple documentation I only need to use make or push the created object inside of array, but any ones work. This is my code and the Automator error:
Option 1:
var Numbers = Application('Numbers');
Numbers.Range({name: 'A2:A20'}).make();
// -> Error: Can't make or move that element into that container
Option 2:
var Numbers = Application('Numbers');
var myRange = Numbers.Range({name: 'A2:A20'});
Numbers.documents[0].sheets[0].tables[0].ranges.push(myRange);
// -> Error: Can't create object.
Option 3:
var Numbers = Application('Numbers');
var myRange = Numbers.Range({name: 'A2:A20'});
Numbers.documents[0].sheets[0].tables[0].selectionRange = myRange;
// -> Automator close with an unexpected error
According to AppleScript documentation (syntax is very different to Javascript) I can do it assigning text that represent the range:
set selection range of table 1 to range "H5:K8"
But if I do something like that with Javascript, it does not work:
Option 4:
var Numbers = Application('Numbers');
Numbers.documents[0].sheets[0].tables[0].selectionRange = 'A2:A20'
// -> Error: Can't convert types.
I have searched for it, but I have not found anything that help me (good references are about AppleScript and the few references that contain something about JXA are about Mail).
Thank you for your help (any link with documentation or any ides to try will be appreciate).

This AppleScript:
tell application "Numbers"
tell table 1 of sheet 1 of document 1
set selection range to range "A2:A20"
end tell
end tell
is actually setting the table's selection range to the table's range named "A2:A20".
Here is the equivalent JavaScript:
var Numbers = Application("Numbers")
var table = Numbers.documents[0].sheets[0].tables[0]
table.selectionRange = table.ranges["A2:A20"]

Related

Automatically copy values from an importrange column to another column before it updates [duplicate]

I have a sheet with data that is imported through IMPORTRANGE to another sheet. When I make changes to Spreadsheet 1 >>> Spreadsheet 2 I have a script that copy the data from Copy to Paste. It is working all fine however I want to make sure that the script only runs when changes are made in the 'Copy' sheet and not any other. At the moment it runs independent on what sheet I make changes in.
Spreadsheet 1
Spreadsheet2
I tried onChange trigger two different ways...
This one makes the change but triggers when changes are made in any of the sheets
function Trigger2(e) {
var sheet = e.source.getActiveSheet();
if (sheet.getName() === 'Copy') {
copyInfo();
}
}
AND
(this does not work)
function Trigger2(e) {
var sheet = e.source.getActiveSheet();
var sheet = SpreadsheetApp.getActiveSpreadsheet();
if( sheet.getActiveSheet().getName() !== "Copy" ) return;{
copyInfo();
}}
The copy code looks like this...
function copyInfo() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var copySheet = ss.getSheetByName("Copy");
var pasteSheet = ss.getSheetByName("Paste");
// get source range
var source = copySheet.getRange(2,1,12,8);
// get destination range
var destination = pasteSheet.getRange(2,1,12,8);
// copy values to destination range
source.copyTo(destination);
}
Copy Changes from one Sheet To Another after Imported Range Changes
Spreadsheet 2 Imports a range to Sheet1 from Spreadsheet 1 Sheet1 and when a change occurs in Spreadsheet 2 Sheet1 we copy data from Spreadsheet2 Sheet1 to Spreadsheet2 Sheet 2.
function onMyChange(e) {
//Logger.log(JSON.stringify(e));//useful during setup
//e.source.toast("Entry");//useful during setup
const sh = e.source.getSheetByName("Sheet1")
Logger.log(sh.getName());
if(e.changeType == "OTHER") {
let tsh = e.source.getSheetByName("Sheet2");
sh.getRange(2,1,12,8).copyTo(tsh.getRange(2,1));//Only need the upper left hand corner of the range. Thus you change change the size of the source data without having to change the target range.
}
}
One might be inclined to attempt to use e.source.getActiveSheet() unfortunately this does not necessary get the sheet that you have written unless in is SpreadsheetApp.getActive().getSheets()[0]. onChange event object always returns the most left sheet in the spreadsheet in this situation so it can't be used to identify the sheet that is being written to from the importRange function.
With this function you no longer require another function.
A simple if should work, but you're trying to compare the name to the wrong field. According to Google's documentation, the source field in the Event Object e returns the Spreadsheet object, which is the entire document.
Instead you can use the e.range field, which contains the exact range where the edit was made, and the Range class has a getSheet() method to get the parent Sheet.
So you can modify your sample to the following:
function Trigger2(e) {
var sheet = e.range.getSheet();
if (sheet.getName() === 'Copy') {//Edit: this will only work if sheet is the most left sheet in the spreadsheet
copyInfo();
}
}

What is the best way to sort a specific range on one sheet in a worksheet?

I am trying to auto sort a small range of data on one specific sheet within another (massive) workbook. I want the range (P27:R40) to sort descendingly every time the sheet is edited, but none of the code I've used has worked. Do I have to include the worksheet name as well as the specific sheet name? (To clarify, I am trying to edit a sheet called "May20" within a worksheet named "Rankings" -- am I missing something here?) Here is the code I've tried using, but it doesn't run upon editing.
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("May20")
var range = sheet.getRange("P27:R40");
// Sorts by the values in column 18 (R)
range.sort({column: 18, ascending: false});
}

kendo ui editor how to modify user selection with range object

Kendo UI 2015.2.805 Kendo UI Editor for Jacascript
I want to extend the kendo ui editor by adding a custom tool that will convert a user selected block that spans two or more paragraphs into block of single spaced text. This can be done by locating all interior p tags and converting them into br tags, taking care not to change the first or last tag.
My problem is working with the range object.
Getting the range is easy:
var range = editor.getRange();
The range object has a start and end container, and a start and end offset (within that container). I can access the text (without markup)
console.log(range.toString());
Oddly, other examples I have seen, including working examples, show that
console.log(range);
will dump the text, however that does not work in my project, I just get the word 'Range', which is the type of the object. This concerns me.
However, all I really need however is a start and end offset in the editor's markup (editor.value()) then I can locate and change the p's to br's.
I've read the telerik documentation and the referenced quirksmode site's explanation of html ranges, and while informative nothing shows how to locate the range withing the text (which seems pretty basic to me).
I suspect I'm overlooking something simple.
Given a range object how can I locate the start and end offset within the editor's content?
EDIT: After additional research it appears much more complex than I anticipated. It seems I must deal with the range and/or selection objects rather than directly with the editor content. Smarter minds than I came up with the range object for reasons I cannot fathom.
Here is what I have so far:
var range = letterEditor.editor.getRange();
var divSelection;
divSelection = range.cloneRange();
//cloning may be needless extra work...
//here manipulate the divSelection to how I want it.
//divSeletion is a range, not sure how to manipulate it
var sel = letterEditor.editor.getSelection()
sel.removeAllRanges();
sel.addRange(divSelection);
EDIT 2:
Based on Tim Down's Solution I came up with this simple test:
var html;
var sel = letterEditor.editor.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
html = container.innerHTML;
}
html = html.replace("</p><p>", "<br/>")
var range = letterEditor.editor.getRange();
range.deleteContents();
var div = document.createElement("div");
div.innerHTML = html;
var frag = document.createDocumentFragment(), child;
while ((child = div.firstChild)) {
frag.appendChild(child);
}
range.insertNode(frag);
The first part, getting the html selection works fine, the second part also works however the editor inserts tags around all lines so the result is incorrect; extra lines including fragments of the selection.
The editor supports a view html popup which shows the editor content as html and it allows for editing the html. If I change the targeted p tags to br's I get the desired result. (The editor does support br as a default line feed vs p, but I want p's most of the time). That I can edit the html with the html viewer tool lets me know this is possible, I just need identify the selection start and end in the editor content, then a simple textual replacement via regex on the editor value would do the trick.
Edit 3:
Poking around kendo.all.max.js I discovered that pressing shift+enter creates a br instead of a p tag for the line feed. I was going to extend it to do just that as a workaround for the single-space tool. I would still like a solution to this if anyone knows, but for now I will instruct users to shift-enter for single spaced blocks of text.
This will accomplish it. Uses Tim Down's code to get html. RegEx could probably be made more efficient. 'Trick' is using split = false in insertHtml.
var sel = letterEditor.editor.getSelection();
if (sel.rangeCount) {
var container = document.createElement("div");
for (var i = 0, len = sel.rangeCount; i < len; ++i) {
container.appendChild(sel.getRangeAt(i).cloneContents());
}
var block = container.innerHTML;
var rgx = new RegExp(/<br class="k-br">/gi);
block = block.replace(rgx, "");
rgx = new RegExp(/<\/p><p>/gi);
block = block.replace(rgx, "<br/>");
rgx = new RegExp(/<\/p>|<p>/gi);
block = block.replace(rgx, "");
letterEditor.editor.exec("insertHtml", { html: block, split: false });
}

Copy/paste data validation in Google Spreadsheets

I feel a bit silly not being able to figure this out. So this is the data validation I have set up:
Cell Range: Journal!J2
Criteria: List from a range - Journal!W2:X2
Cell Range: Journal!M2
Criteria: List from a range - Journal!Y2:AA2
This is great in my first row. I create another row and I'd like it to update all of the '2' to '3'. The cell range updates correctly, but the criteria does not, and I can't figure out an easy solution other than going in and updating it manually.
I've tried copy/paste as well as paste special -> data validation.
I know something like $Y$2 would fix the row/col but that's the opposite of what I want. I guess I'm wanting to maintain the relative formula vs it being an absolute formula?
Indeed, the "list from a range" type of validation treats the reference to the list as absolute rather than relative. I know two workarounds:
Custom formula
Validation based on the custom formula
=not(isna(match(J2, W2:X2, 0)))
is equivalent to "value must be from the range W2:X2", and it will be copied down correctly, the reference to W2:X2 being relative.
Drawback: you don't get an in-cell dropdown list with custom formula validation.
Script
One can use an Apps Script to manage data validation rules. The following script sets data validation rules in each cell of the range J2:J100, where the value is required to be from W:X of the same row.
function validate() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("J2:J100");
var valuesColumn = 23; // begins in W
var valuesLength = 2; // has length 2, so W:X
var firstRow = range.getRow();
for (var i = 0; i < range.getHeight(); i++) {
var rule = SpreadsheetApp.newDataValidation()
.requireValueInRange(sheet.getRange(firstRow + i, valuesColumn, 1, valuesLength), true)
.setAllowInvalid(false)
.build();
range.offset(i, 0, 1, 1).setDataValidation(rule);
}
}

Using Javascript for Automation to copy cells in Numbers

I want to use JXA to automate some updating of Numbers spreadsheets. For example, copying a range of cells from one spreadsheet to another one with a different structure.
At this point, I'm just testing a simple program to set or read the value of a cell and I can't get this to work.
When I try to set a value I get "Error -1700: Can't convert types." and when I try to read a value I get back a [object ObjectSpecifier] rather than a text or number value.
Here's an example of the code:
Numbers = Application('Numbers')
Numbers.activate()
delay(1)
doc = Numbers.open(Path('/Users/username/Desktop/Test.numbers'))
currentSheet = doc.Sheets[0]
currentTable = currentSheet.Tables[0]
console.log(currentTable['name'])
console.log(currentTable.cell[1][1])
currentTable.cell[1][1].set(77)
When I run this, I get and output of [object ObjectSpecifier] for the two console.logs and then an error -1700: Can't convert types when it tries to set a cell.
I've tried several other variations of accessing or setting properties but can't get it to work.
Thanks in advance,
Dave
Here is a script that sets and gets a cell's value and then sets a different cell's value in the same table:
// Open Numbers document (no activate or delay is needed)
var Numbers = Application("Numbers")
var path = Path("/path/to/spreadsheet.numbers")
var doc = Numbers.open(path)
// Access the first table of the first sheet of the document
// Note:
// .sheets and .tables (lowercase plural) are used when accessing elements
// .Sheet and .Table (capitalized singular) are used when creating new elements
var sheet = doc.sheets[0]
var table = sheet.tables[0]
// Access the cell named "A1"
var cell = table.cells["A1"]
// Set the cell's value
cell.value = 20
// Get the cell's value
var cellValue = cell.value()
// Set that value in a different cell
table.cells["B2"].value = cellValue
Check out the Numbers scripting dictionary (with JavaScript selected as the language) to see classes and their properties and elements. The elements section will show you the names of elements (e.g. the Document class contains sheets, the Sheet class contains tables, and so on). To open the scripting dictionary, in Script Editor's menu bar, choose Window > Library, and then select Numbers in the library window.
In regards to the logging you were seeing - I recommend using a function similar to this:
function prettyLog(object) {
console.log(Automation.getDisplayString(object))
}
Automation.getDisplayString gives you a "pretty print" version of any object you pass to it. You can then use that for better diagnostic logging.

Resources