Text

OmniFocus projects and tasks contain an element for storing and displaying text content, the note, whose text contents are accessed through the note property of the Project and Task classes.

The value or contents of the note property is text in an unaltered or “plain” format, instances of the String class.

While the value of the note property can be both read and written by scripts, any editing of the Note contents must be done by the script (using JavaScript functions) outside of the note container and then the entire existing Note content is replaced by the altered version.

Editing Note Plain Text (Strings)


task = document.windows[0].selection.tasks[0]) noteContent = task.note //--> "How Now Brown Cow" newContent = noteContent.replace("Brown", "Green") task.note = newContent task.note //--> "How Now Green Cow"

In addition to the lack of formatting and styling, another limitation of plain text (Strings) is the use of links embedded within the text of the note. The plain text strings of the note property do not support link attachments, where the URL of the link is not displayed along with the link title, but is activated when the link title is tapped or clicked, like this: OmniFocus Voice Control Collection

 

Text Objects

Beginning with OmniFocus 4, the textual content of project and task notes supports the standard Rich Text Format (RFT). This means that text in notes can be sized, colored, and styled, and may also contain specialized text objects such as links.

Access to the styled text content of Project and Task notes is provided by the addition of two identical properties:

IMPORTANT: These properties do not replace the current note property, but are provided as additional access points to note content.

The value of the the noteText property is a Text Object. Text Objects provide a mechanism for associating formatting data with text strings.

A text object is a container that holds text and related style information, as well as optional references to attachments, and even other text objects.

Creating a Text Object

To create a text object, instantiate an instance of the Text class by providing the text string and the assigned style object. In the example shown below, the style of a specified Project or Task note is used:

When the following script example is run in the automation console, the result is an object reference to the created text object followed by a properties record displaying the default property values of the created text object.

Create New Text Object for Note


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText textObj = new Text('How Now Brown Cow', noteObj.style) //--> [object Text] {attachments: [], attributeRuns: [[object Text]], characters: [[object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], [object Text], …], end: [object Text.Position], fileWrapper: null, paragraphs: [[object Text]], range: [object Text.Range], sentences: [[object Text]], start: [object Text.Position], string: "How Now Brown Cow", style: [object Style], words: [[object Text], [object Text], [object Text], [object Text]]} task.noteText = textObj }

NOTE: as you can see in the resulting properties record, text objects may contain other text objects.

Text Instance Properties

As with most scriptable objects, a text object has properties whose values can be accessed. However, in the case of text objects, editing the value of a property will not change the displayed text. To change the display of text objects, the Text.Range class will be used as demonstrated later in this section.

 

TextComponent Class

Text Components are the elements contained with text objects.

Using the string property of a Text Object and the JavaScript map() function to convert the arrays of returned text objects to arrays of text strings:

Converting Text Objects to String Arrays


textObj = new Text('How Now Brown Cow.', document.outline.baseStyle) textObj.characters.map(txtObj => txtObj.string) //--> ["H","o","w"," ","N","o","w"," ","B","r","o","w","n"," ","C","o","w","."] textObj.words.map(txtObj => txtObj.string) //--> ["How","Now","Brown","Cow"] textObj.sentences.map(txtObj => txtObj.string) //--> ["How Now Brown Cow."] textObj.paragraphs.map(txtObj => txtObj.string) //--> ["How Now Brown Cow."]

When using Text Components to apply styling to text content, it is important which type of text component is targeted.

Here’s an example of using the TextComponent class to colorize every word of a note to random color values. The script targets TextComponent.Words as the recipient of the styling settings:

Apply a Random Color to Each Word


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ function randomColor(){ r = Math.floor(Math.random() * 101) * 0.01 g = Math.floor(Math.random() * 101) * 0.01 b = Math.floor(Math.random() * 101) * 0.01 return Color.RGB(r, g, b, 1) } noteObj = items[0].noteText wordRanges = noteObj.ranges(TextComponent.Words) wordRanges.forEach(range => { aColor = randomColor() txtObjStyle = noteObj.styleForRange(range) txtObjStyle.set(Style.Attribute.FontFillColor, aColor) }) }
Note text with random colored words

IMPORTANT: To apply paragraph styling to text without overriding font styling, use TextComponent.AttributeRuns as the targeted text components:

Apply Paragraph Styles


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText runRanges = noteObj.ranges(TextComponent.AttributeRuns) runRanges.forEach(range => { runStyle = noteObj.styleForRange(range) runStyle.set(Style.Attribute.ParagraphLineSpacing, 24) runStyle.set(Style.Attribute.ParagraphFirstLineHeadIndent, 36) }) }

Text Functions

Omni Automation support in OmniOutliner includes a set of functions (commands) for manipulating text objects.

Class Functions

Instance Functions

 

Text.FindOption Class

Here are the class properties for setting the parameters for a search using the find() function of the Text class:

 

Text.Range Class

Ranges are an essential mechanism for representing text objects for manipulation.

 

Text.Position Class

Indicates the insertion point for a Text object.

omnifocus://localhost/omnijs-run?script=task%20%3D%20new%20Task%28%22EXAMPLE%20TASK%22%29%0Atask%2Enote%20%3D%20%22How%20Now%20Brown%20Cow%22%0AnoteObj%20%3D%20task%2EnoteText%0AnewTextObj%20%3D%20new%20Text%28%27START%2D%2D%3E%27%2C%20noteObj%2Estyle%29%0AnoteObj%2Einsert%28noteObj%2Estart%2C%20newTextObj%29%0AnewTextObj%20%3D%20new%20Text%28%27%3C%2D%2DEND%27%2C%20noteObj%2Estyle%29%0AnoteObj%2Einsert%28noteObj%2Eend%2C%20newTextObj%29%0AURL%2EfromString%28%60%24%7BURL%2EcurrentAppScheme%7D%3A%2F%2F%2Ftask%2F%24%7Btask%2Eid%2EprimaryKey%7D%60%29%2Eopen%28%29
Inserting Text Objects using Text.Position
 

task = new Task("EXAMPLE TASK") task.note = "How Now Brown Cow" noteObj = task.noteText newTextObj = new Text('START-->', noteObj.style) noteObj.insert(noteObj.start, newTextObj) newTextObj = new Text('<--END', noteObj.style) noteObj.insert(noteObj.end, newTextObj) URL.fromString(`${URL.currentAppScheme}:///task/${task.id.primaryKey}`).open()

Working with Note Objects

Here’s a simple example of editing a text object by prepending it with another text object assigned with the same style as the target text object. Note the use of the start and style text object properties, and the insert() instance function:

Prepend to Text Object (note)


task = new Task("Example Task") task.note = "How Now Brown Cow" noteObj = task.noteText noteObj.insert(noteObj.start, new Text("Well ", noteObj.style)) noteObj.string //--> "Well How Now Brown Cow"

Removing Text Objects from Note

To remove the previously added word “Well” select the previously created task and run the following script, which uses the remove() function to remove the first word as well as the space following it:

Remove Texts Objects from Note


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText // remove the first word noteObj.remove(noteObj.words[0].range) // remove the space after the removed first word noteObj.remove(noteObj.characters[0].range) }

The task note is returned to its prior state.

DO THIS: In preparation for demonstrating the use of styles, summon the OmniFocus Color palette from the Format menu and apply three different colors to each of the last three words of the the task’s note.

Adding a Text Object to Existing Note

Using the append() and insert() functions of the Text class, it is easy to add a newly created text object to the text object of the note.

Also note the use of the isEmpty property of the Text.Range class to determine if there is existing note content.

Select the task and run this script:

Adding Created Text Object to Note


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText newTextObj = new Text('Rain in Spain Stays in the Plain', noteObj.style) if(noteObj.range.isEmpty){ noteObj.insert(noteObj.start, newTextObj) } else { newLineObj = new Text('\n', noteObj.style) noteObj.append(newLineObj, noteObj.style) noteObj.append(newTextObj, noteObj.style) } }

(⬇ see below ) Content added to task note:

Added text to note

NOTE: Content added using Text Objects does not interfere with the formatting of the existing text.

Next, let’s replace the word “Brown” with the word: “Wonderful”

To perform the replacement of one text object with another, use the find() and replace() functions:

Replace Text with Text


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText findParams = [Text.FindOption.CaseInsensitive] range = noteObj.find("Brown", findParams, noteObj.range) if(range){ newTextObj = new Text('Wonderful', noteObj.style) noteObj.replace(range, newTextObj) } }

(⬇ see below ) Content replaced in task note:

Replaced text in note

All good! However, notice that the existing formatting applied to the word “Brown” did not automatically transfer to the replacement word: “Wonderful”

To accomplish the application of the existing style to the replacement text, use the styleForRange() function of the Text class and the setStyle() function of the Style class to store and apply the existing style settings of the targeted word.

Replace Retaining Current Style


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText findParams = [Text.FindOption.CaseInsensitive] range = noteObj.find("Brown", findParams, noteObj.range) if(range){ currentStyle = noteObj.styleForRange(range) newTextObj = new Text('Wonderful', noteObj.style) newTextObj.styleForRange(newTextObj.range).setStyle(currentStyle) noteObj.replace(range, newTextObj) } }

(⬇ see below ) Content replaced in task note:

Replaced styled text in note

NOTE: Simply setting the value of the style property does not apply the values. To apply styling to text, use the setStyle() function on the range of the text.

Matching by Words (Word Ranges)

A common automated process is searching a range of text for all occurrences of a specific word, and processing each found instance.

Iterating an array of word ranges derived through the use of the TextComponent class can provide the mechanism accomplishing this challenge.

Case Sensitive Word Match


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText wordToMatch = 'commodo' wordRanges = noteObj.ranges(TextComponent.Words) wordRanges.forEach(range => { if(noteObj.textInRange(range).string === wordToMatch){ txtObjStyle = noteObj.styleForRange(range) txtObjStyle.set(Style.Attribute.FontFillColor, Color.magenta) txtObjStyle.set(Style.Attribute.FontWeight, 9) } }) } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

(⬇ see below ) Occurrences of the targeted word are stylized. NOTE: that the search was case-sensitive, missing the occurrence of the targeted word that was capitalized. (orange oval)

Case Sensitive Word Matching

To perform a case-insensitive search that recognizes case variations of the the targeted word, here are two options:

In this example, both methods work. NOTE: the Text.FindOption class provides an extensive set of parameters to refine your searches.

Case Insensitive Word Match (RegEx)


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText wordToMatch = /commodo/i wordRanges = noteObj.ranges(TextComponent.Words) wordRanges.forEach(range => { wordString = noteObj.textInRange(range).string result = wordString.match(wordToMatch) if(result){ txtObjStyle = noteObj.styleForRange(range) txtObjStyle.set(Style.Attribute.FontFillColor, Color.magenta) txtObjStyle.set(Style.Attribute.FontWeight, 9) } }) } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }
Case Insensitive Word Match (Find Options)


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText wordToMatch = 'commodo' findParams = [Text.FindOption.CaseInsensitive] wordRanges = noteObj.ranges(TextComponent.Words) wordRanges.forEach(range => { result = noteObj.find(wordToMatch, findParams, range) if(result){ txtObjStyle = noteObj.styleForRange(range) txtObjStyle.set(Style.Attribute.FontFillColor, Color.magenta) txtObjStyle.set(Style.Attribute.FontWeight, 9) } }) } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

(⬇ see below ) All occurrences of the targeted word were processed, even those that were capitalized. (orange oval)

Case Insensitive Word Matching

Matching by Strings

To search and match phrases containing one or more words, use the find() function of the Text class.

Since the find() function returns only the first matching reference, it is placed within a JavaScript while loop that repeats the search until no more matches are found.

The key to making this technique work is to adjust the range to be searched to begin with the end of the previously matched range (line 21).

Search for All Occurrences of a String


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText console.log(noteObj) stringToMatch = 'consectetur adipiscing commodo' findParams = [Text.FindOption.CaseInsensitive] rangeToSearch = noteObj.range resultRange = noteObj.range while (resultRange !== null) { resultRange = noteObj.find(stringToMatch, findParams, rangeToSearch) if(resultRange){ txtObjStyle = noteObj.styleForRange(resultRange) txtObjStyle.set(Style.Attribute.FontFillColor, Color.magenta) txtObjStyle.set(Style.Attribute.FontWeight, 9) // adjust search range to begin from end of last matched range rangeToSearch = new Text.Range(resultRange.end, noteObj.range.end ) } } } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

(⬇ see below ) All occurrences of the phrase are processed:

Find and manipulate all instances of a string  

Link Objects

We’ve all encountered Link Objects embedded in formatted text, similar to this one: OmniFocus Automation. The link title displayed is often different from the actual URL that the link calls when it is activated through tap or click.

Link Objects are Text Objects with the Link style attribute applied to the range of the text object. The text content of the Text Object is displayed, but the value of the Link style attribute is what gets sent to the operating system to handle.

Here’s an example script for locating all link instances in a note and applying style attributes. It works by checking the value of the Link style attribute for every attribute run in the note.

Apply Styling to Links


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText runRanges = noteObj.ranges(TextComponent.AttributeRuns) runRanges.forEach(range => { runStyle = noteObj.styleForRange(range) linkStr = runStyle.get(Style.Attribute.Link).string if(linkStr.length > 0){ console.log(noteObj.textInRange(range).string) // mutable attributes runStyle.set(Style.Attribute.FontFamily, "Helvetica") runStyle.set(Style.Attribute.FontWeight, 9) curSize = runStyle.get(Style.Attribute.FontSize) newSize = curSize * 1.5 runStyle.set(Style.Attribute.FontSize, newSize) runStyle.set(Style.Attribute.FontItalic, true) runStyle.set(Style.Attribute.KerningAdjustment, 4) } }) }

NOTE: Color attributes cannot be applied to link objects.

The following script demonstrates the creation and addition of a Link Object to the note of the selected OmniFocus task or project:

Appending a Link to a Note


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText linkURL = URL.fromString("https://omni-automation.com") linkObj = new Text("(Omni Automation)", noteObj.style) newLineObj = new Text("\n", noteObj.style) style = linkObj.styleForRange(linkObj.range) style.set(Style.Attribute.Link, linkURL) if (noteObj.string.length > 0){noteObj.append(newLineObj)} noteObj.append(linkObj) node = document.windows[0].content.selectedNodes[0] node.expandNote(false) } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

Links that Do Things (Actionable Links)

NOTE The URLs of link objects you add to OmniFocus item notes, may be links that trigger actions in 3rd-party applications or services.

Since all Omni Automation scripts can be converted into script URLs, they can also be embedded as actionable links into a note.

The following script example will append a link to the selected project or task. Since the link is an Omni Automation Script URL, it requires user approval to execute (documentation).

When the link is activated, a script security dialog in which you can view the script code and approve or deny its execution, will be presented. If you approve the script, it will present a greeting alert containing its name. There is an option to approve the script to run without further approval.

Embedded Omni Automation scripts may perform one or more functions for you, such as automatically sharing info with teammates about the status of the task or project — an “Update Team” link.

Here’s the alert presented by the example script:

Script Greeting Alert
Append Omni Automation Script Link


sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ itemID = items[0].id.primaryKey noteObj = items[0].noteText if(items[0] instanceof Task){ scriptURLStr = `omnifocus://localhost/omnijs-run?script=itemTitle%20%3D%20Task%2EbyIdentifier%28%22${itemID}%22%29%2Ename%0Dnew%20Alert%28%22Greeting%22%2C%20%60I%E2%80%99m%20a%20task%20named%20%E2%80%9C%24%7BitemTitle%7D%E2%80%9D%60%29%2Eshow%28%29` } else { scriptURLStr = `omnifocus://localhost/omnijs-run?script=itemTitle%20%3D%20Project%2EbyIdentifier%28%22${itemID}%22%29%2Ename%0Dnew%20Alert%28%22Greeting%22%2C%20%60I%E2%80%99m%20a%20project%20named%20%E2%80%9C%24%7BitemTitle%7D%E2%80%9D%60%29%2Eshow%28%29` } console.log(scriptURLStr) linkURL = URL.fromString(scriptURLStr) linkObj = new Text("(GREETING)", noteObj.style) newLineObj = new Text("\n", noteObj.style) style = linkObj.styleForRange(linkObj.range) style.set(Style.Attribute.Link, linkURL) if (noteObj.string.length > 0){noteObj.append(newLineObj)} noteObj.append(linkObj) node = document.windows[0].content.selectedNodes[0] node.expandNote(false) } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

Here are the two scripts (one for tasks, one for projects) of which one is converted into an Omni Automation script URL and used as the URL of the appended link object:

Greeting Scripts


// Replace "XXXXX" in encoded script with: ${itemID} itemTitle = Task.byIdentifier("XXXXX").name new Alert("Greeting", `I’m a task named “${itemTitle}”`).show() // Replace "XXXXX" in encoded script with: ${itemID} itemTitle = Project.byIdentifier("XXXXX").name new Alert("Greeting", `I’m a project named “${itemTitle}”`).show()

Here's a script that prepends a link to the start of the note of the selected project or task using the URL currently on the clipboard:

Prepend Clipboard Link to Note
  

(async () => { try { var sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ item = sel.databaseObjects[0] targetType = TypeIdentifier.URL if(Pasteboard.general.availableType([targetType])){ var items = Pasteboard.general.items var i for (i = 0; i < items.length; i++) { var data = items[i].dataForType(targetType) if(data){ console.log(data.toString()) break } } urlStr = data.toString() noteObj = item.noteText linkURL = URL.fromString(urlStr) linkObj = new Text("(LINK)", noteObj.style) newLineObj = new Text("\n", noteObj.style) style = linkObj.styleForRange(linkObj.range) style.set(Style.Attribute.Link, linkURL) noteObj.insert(noteObj.start, linkObj) noteObj.insert(linkObj.range.end, newLineObj) node = document.windows[0].content.selectedNodes[0] node.expandNote(false) } else { throw { name: "Missing URL", message: "There are no URLs on the clipboard." } } } else { throw { name: "Selection Issue", message: "Please select a single project or task." } } } catch(err){ new Alert(err.name, err.message).show() } })();

And here’s another Link script that replaces all links in markdown format with link objects:

Replace Markdown Links with Link Objects
  

sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText regexMdLinks = /\[([^\[]+)\](\(.*\))/gm strToMatch = regexMdLinks.source findParams = [Text.FindOption.RegularExpression] rangeToSearch = noteObj.range resultRange = noteObj.range while (resultRange !== null) { resultRange = noteObj.find(regexMdLinks.source, findParams, rangeToSearch) if(resultRange){ mdLinkStr = noteObj.textInRange(resultRange).string x = mdLinkStr.indexOf("]") y = mdLinkStr.indexOf("(") mdLinkTitle = mdLinkStr.substr(1, x - 1) mdLinkURLStr = mdLinkStr.substr(y + 1, mdLinkStr.length) mdLinkURLStr = mdLinkURLStr.slice(0, -1) linkURL = URL.fromString(mdLinkURLStr) if (linkURL){ linkObj = new Text(mdLinkTitle, noteObj.styleForRange(resultRange)) style = linkObj.styleForRange(linkObj.range) style.set(Style.Attribute.Link, linkURL) noteObj.replace(resultRange, linkObj) } // adjust search range to begin from end of last matched range rangeToSearch = new Text.Range(resultRange.end, noteObj.range.end ) } } } else { throw { name: "Selection Issue", message: "Please select a single project or task." } }

And conversely, here’s a script that replaces all link objects with markdown:

Replace Link Objects with Markdown
  

sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText runRanges = noteObj.ranges(TextComponent.AttributeRuns) runRanges.reverse().forEach(range => { runStyle = noteObj.styleForRange(range) linkStr = runStyle.get(Style.Attribute.Link).string if(linkStr.length > 0){ linkTitle = noteObj.textInRange(range).string mdLinkStr = `[${linkTitle}](${linkStr})` linkObj = new Text(mdLinkStr, runStyle) noteObj.replace(range, linkObj) } }) }
 

Attachments

Since note content is now in RTF format, inline file attachments can be inserted into the note content.

In the following Text attachment example, a POST method is used with the fetch() function (documentation) to retrieve a chart generated by the QuickChart website (API documentation). The generated image is then added as a text attachment to the note of the currently selected project or task.

NOTE: The retrieved image data is used to create a file wrapper (line 20) which is then used with the makeFileAttachment() function of the Text class to generate an “attachment” text object (line 21) containing the image. The text object is then appended to the end of the note (line 25).

omnifocus://localhost/omnijs-run?script=%28async%20%28%29%20%3D%3E%20%7B%0D%09try%20%7B%0D%09%09sel%20%3D%20document%2Ewindows%5B0%5D%2Eselection%0D%09%09items%20%3D%20sel%2EdatabaseObjects%2Efilter%28%0D%09%09%09item%20%3D%3E%20%28item%20instanceof%20Project%20%7C%7C%20item%20instanceof%20Task%29%0D%09%09%29%0D%09%09if%28items%2Elength%20%3D%3D%3D%201%29%7B%0D%09%09%09noteObj%20%3D%20items%5B0%5D%2EnoteText%0D%09%09%09nStyle%20%3D%20noteObj%2Estyle%0D%0D%09%09%09var%20chartData%20%3D%20%7B%22backgroundColor%22%3A%22transparent%22%2C%22width%22%3A600%2C%22height%22%3A300%2C%22format%22%3A%22png%22%2C%22chart%22%3A%7B%22type%22%3A%22line%22%2C%22data%22%3A%7B%22labels%22%3A%5B%22January%22%2C%22February%22%2C%22March%22%2C%22April%22%2C%22May%22%2C%22June%22%2C%22July%22%5D%2C%22datasets%22%3A%5B%7B%22label%22%3A%22A%22%2C%22data%22%3A%5B%2D15%2C%2D80%2C79%2C%2D11%2C%2D5%2C33%2C%2D57%5D%2C%22backgroundColor%22%3A%22rgb%28255%2C%2099%2C%20132%29%22%2C%22borderColor%22%3A%22rgb%28255%2C%2099%2C%20132%29%22%2C%22fill%22%3Afalse%2C%22borderDash%22%3A%5B5%2C5%5D%2C%22pointRadius%22%3A15%2C%22pointHoverRadius%22%3A10%7D%2C%7B%22label%22%3A%22B%22%2C%22data%22%3A%5B%2D86%2C59%2C%2D70%2C%2D40%2C40%2C33%2C16%5D%2C%22backgroundColor%22%3A%22rgb%2854%2C%20162%2C%20235%29%22%2C%22borderColor%22%3A%22rgb%2854%2C%20162%2C%20235%29%22%2C%22fill%22%3Afalse%2C%22borderDash%22%3A%5B5%2C5%5D%2C%22pointRadius%22%3A%5B2%2C4%2C6%2C18%2C0%2C12%2C20%5D%7D%2C%7B%22label%22%3A%22C%22%2C%22data%22%3A%5B59%2C%2D65%2C%2D33%2C0%2C%2D79%2C95%2C%2D53%5D%2C%22backgroundColor%22%3A%22rgb%2875%2C%20192%2C%20192%29%22%2C%22borderColor%22%3A%22rgb%2875%2C%20192%2C%20192%29%22%2C%22fill%22%3Afalse%2C%22pointHoverRadius%22%3A30%7D%2C%7B%22label%22%3A%22D%22%2C%22data%22%3A%5B73%2C83%2C%2D19%2C74%2C16%2C%2D12%2C8%5D%2C%22backgroundColor%22%3A%22rgb%28255%2C%20205%2C%2086%29%22%2C%22borderColor%22%3A%22rgb%28255%2C%20205%2C%2086%29%22%2C%22fill%22%3Afalse%2C%22pointHitRadius%22%3A20%7D%5D%7D%2C%22options%22%3A%7B%22legend%22%3A%7B%22position%22%3A%22bottom%22%7D%2C%22title%22%3A%7B%22display%22%3Atrue%2C%22text%22%3A%22Sales%20Projections%22%7D%7D%7D%7D%0D%09%0D%09%09%09targetURLString%20%3D%20%22https%3A%2F%2Fquickchart%2Eio%2Fchart%22%0D%09%09%09request%20%3D%20URL%2EFetchRequest%2EfromString%28targetURLString%29%0D%09%09%09request%2Eheaders%20%3D%20%7B%22Content%2DType%22%3A%22application%2Fjson%22%7D%0D%09%09%09request%2Emethod%20%3D%20%27POST%27%0D%09%09%09request%2EbodyString%20%3D%20JSON%2Estringify%28chartData%29%0D%09%09%09response%20%3D%20await%20request%2Efetch%28%29%0D%09%09%09responseCode%20%3D%20response%2EstatusCode%0D%09%09%09if%20%28responseCode%20%3E%3D%20200%20%26%26%20responseCode%20%3C%20300%29%7B%0D%09%09%09%09wrapper%20%3D%20FileWrapper%2EwithContents%28%22ex%2Dchart%2Epng%22%2C%20response%2EbodyData%29%0D%09%09%09%09attachmentObj%20%3D%20Text%2EmakeFileAttachment%28wrapper%2C%20noteObj%2Estyle%29%0D%09%09%09%0D%09%09%09%09if%28noteObj%2Erange%2EisEmpty%29%7B%0D%09%09%09%09%09noteObj%2Einsert%28noteObj%2Estart%2C%20attachmentObj%29%0D%09%09%09%09%7D%20else%20%7B%0D%09%09%09%09%09newLineObj%20%3D%20new%20Text%28%27%5Cn%27%2C%20noteObj%2Estyle%29%0D%09%09%09%09%09noteObj%2Eappend%28newLineObj%2C%20noteObj%2Estyle%29%0D%09%09%09%09%09noteObj%2Eappend%28attachmentObj%2C%20noteObj%2Estyle%29%0D%09%09%09%09%7D%0D%09%09%09%7D%20else%20%7B%0D%09%09%09%09throw%20%7B%0D%09%09%09%09%09name%3A%20%22Server%20Response%20Code%22%2C%0D%09%09%09%09%09message%3A%20%60The%20server%20responded%20with%20code%3A%20%24%7BresponseCode%7D%60%0D%09%09%09%09%7D%0D%09%09%09%7D%0D%09%09%0D%09%09%09node%20%3D%20document%2Ewindows%5B0%5D%2Econtent%2EselectedNodes%5B0%5D%0D%09%09%09node%2EexpandNote%28false%29%0D%09%09%7D%20else%20%7B%0D%09%09%09throw%20%7B%0D%09%09%09%09name%3A%20%22Selection%20Issue%22%2C%0D%09%09%09%09message%3A%20%22Please%20select%20a%20single%20project%20or%20task%2E%22%0D%09%09%09%7D%0D%09%09%7D%0D%09%7D%0D%09catch%28err%29%7B%0D%09%09new%20Alert%28err%2Ename%2C%20err%2Emessage%29%2Eshow%28%29%0D%09%7D%0D%7D%29%28%29%3B
Add Chart Attachment
 

(async () => { try { sel = document.windows[0].selection items = sel.databaseObjects.filter( item => (item instanceof Project || item instanceof Task) ) if(items.length === 1){ noteObj = items[0].noteText nStyle = noteObj.style var chartData = {"backgroundColor":"transparent","width":600,"height":300,"format":"png","chart":{"type":"line","data":{"labels":["January","February","March","April","May","June","July"],"datasets":[{"label":"A","data":[-15,-80,79,-11,-5,33,-57],"backgroundColor":"rgb(255, 99, 132)","borderColor":"rgb(255, 99, 132)","fill":false,"borderDash":[5,5],"pointRadius":15,"pointHoverRadius":10},{"label":"B","data":[-86,59,-70,-40,40,33,16],"backgroundColor":"rgb(54, 162, 235)","borderColor":"rgb(54, 162, 235)","fill":false,"borderDash":[5,5],"pointRadius":[2,4,6,18,0,12,20]},{"label":"C","data":[59,-65,-33,0,-79,95,-53],"backgroundColor":"rgb(75, 192, 192)","borderColor":"rgb(75, 192, 192)","fill":false,"pointHoverRadius":30},{"label":"D","data":[73,83,-19,74,16,-12,8],"backgroundColor":"rgb(255, 205, 86)","borderColor":"rgb(255, 205, 86)","fill":false,"pointHitRadius":20}]},"options":{"legend":{"position":"bottom"},"title":{"display":true,"text":"Sales Projections"}}}} targetURLString = "https://quickchart.io/chart" request = URL.FetchRequest.fromString(targetURLString) request.headers = {"Content-Type":"application/json"} request.method = 'POST' request.bodyString = JSON.stringify(chartData) response = await request.fetch() responseCode = response.statusCode if (responseCode >= 200 && responseCode < 300){ wrapper = FileWrapper.withContents("ex-chart.png", response.bodyData) attachmentObj = Text.makeFileAttachment(wrapper, noteObj.style) if(noteObj.range.isEmpty){ noteObj.insert(noteObj.start, attachmentObj) } else { newLineObj = new Text('\n', noteObj.style) noteObj.append(newLineObj, noteObj.style) noteObj.append(attachmentObj, noteObj.style) } node = document.windows[0].content.selectedNodes[0] node.expandNote(false) } else { throw { name: "Server Response Code", message: `The server responded with code: ${responseCode}` } } } else { throw { name: "Selection Issue", message: "Please select a single project or task." } } } catch(err){ new Alert(err.name, err.message).show() } })();
Generated Chart as an inline Attachment