├── README.md ├── bibtex.omnioutlinerjs ├── Resources │ ├── ApplicationLib.js │ ├── ApplicationLib.strings │ ├── Parser.js │ ├── Parser.strings │ ├── copyBIBITEM.js │ ├── copyKey.js │ ├── en.lproj │ │ ├── copyBIBITEM.strings │ │ ├── copyKey.strings │ │ ├── exportBibTeX.strings │ │ ├── importAttachment.strings │ │ ├── importBibTeX.strings │ │ ├── manifest.strings │ │ └── uniqueKey.strings │ ├── exportBibTeX.js │ ├── icon.png │ ├── importAttachment.js │ ├── importBibTeX.js │ └── uniqueKey.js └── manifest.json ├── edit.omnioutlinerjs ├── Resources │ ├── ApplicationLib.js │ ├── ApplicationLib.strings │ ├── addAttachment.js │ ├── addLink.js │ ├── addText.js │ ├── computeColumn.js │ ├── copyAsLink.js │ ├── copyColumn.js │ ├── duplicateColumn.js │ ├── editColumn.js │ ├── en.lproj │ │ ├── addAttachment.strings │ │ ├── addLink.strings │ │ ├── addText.strings │ │ ├── computeColumn.strings │ │ ├── copyAsLink.strings │ │ ├── copyColumn.strings │ │ ├── duplicateColumn.strings │ │ ├── editColumn.strings │ │ ├── findAndReplace.strings │ │ ├── manifest.strings │ │ ├── pasteColumn.strings │ │ └── renameAttachment.strings │ ├── findAndReplace.js │ ├── icon.png │ ├── pasteColumn.js │ └── renameAttachment.js └── manifest.json ├── format.omnioutlinerjs ├── Resources │ ├── ApplicationLib.js │ ├── ApplicationLib.strings │ ├── applyTitleCase.js │ ├── clearStyle.js │ ├── columnFormatter.js │ ├── en.lproj │ │ ├── applyTitleCase.strings │ │ ├── clearStyle.strings │ │ ├── columnFormatter.strings │ │ ├── lineSpacing.strings │ │ ├── manifest.strings │ │ ├── pasteStyle.strings │ │ ├── splitParagraph.strings │ │ └── trimColumn.strings │ ├── icon.png │ ├── lineSpacing.js │ ├── pasteStyle.js │ ├── splitParagraph.js │ └── trimColumn.js └── manifest.json ├── share.omnioutlinerjs ├── Resources │ ├── ApplicationLib.js │ ├── ApplicationLib.strings │ ├── addToAnki.js │ ├── addToDEVONthink.js │ ├── addToThings.js │ ├── en.lproj │ │ ├── addToAnki.strings │ │ ├── addToDEVONthink.strings │ │ ├── addToThings.strings │ │ ├── manifest.strings │ │ ├── shareAsMarkdown.strings │ │ ├── shareAttachment.strings │ │ ├── shareClipboard.strings │ │ ├── shareColumn.strings │ │ └── shareItemLink.strings │ ├── icon.png │ ├── shareAsMarkdown.js │ ├── shareAttachment.js │ ├── shareClipboard.js │ ├── shareColumn.js │ └── shareItemLink.js └── manifest.json └── view.omnioutlinerjs ├── Resources ├── ApplicationLib.js ├── ApplicationLib.strings ├── characterCount.js ├── columnStatistics.js ├── en.lproj │ ├── characterCount.strings │ ├── columnStatistics.strings │ ├── focusSelection.strings │ ├── hideColumn.strings │ ├── manifest.strings │ ├── texToPNG.strings │ └── wordCount.strings ├── focusSelection.js ├── hideColumn.js ├── icon.png ├── texToPNG.js └── wordCount.js └── manifest.json /README.md: -------------------------------------------------------------------------------- 1 | # OmniOutliner Plug-Ins 2 | ## Introduction 3 | This is a collection of [Omni Automation](https://omni-automation.com) scripts for OmniOutliner, organised into 5 bundle plug-ins: 4 | 5 | - Edit 6 | - Format 7 | - View 8 | - Share 9 | - BibTeX 10 | 11 | The functionalities are mostly self-explainatory from filenames. The details of each action can be found below. They are designed for iPadOS but most should also work on MacOS. 12 | 13 | ## Plug-Ins 14 | ### Edit 15 | #### Copy as Link 16 | This action copies the links for the selected rows, e.g. `omnioutliner:///open?row=fUpE2aoNbcL`, with the option of copying as an array of links. 17 | #### Copy Column 18 | This action copies the contents of a selected column in the selected rows as plain texts, with the option of copying as an array of texts. 19 | #### Paste Column 20 | This action pastes the list of objects from clipboard into a selected column for the selected rows, one in each cell, with the option to override existing contents in the cell. If there are more objects in the clipboard than selected rows, the list of clipboard objects is truncated to the number of selected rows, and vice versa. 21 | #### Edit Column 22 | This action takes text inputs and applies repeatedly into the selected target column. If the override toggle is off, then it appends the texts instead. 23 | #### Compute Column 24 | This action takes inputs from selected columns and outputs the computed results into the selected target column, processing on a row by row basis. The formula field allows for any valid JavaScript expression that passes through `eval()`. 25 | #### Find and Replace 26 | This action uses input RegEx to find or replace texts, either all at once, or cell by cell with manual confirmation. 27 | #### Add Text 28 | This action inserts input text into a selected column of the selected rows at either the end or the beginning, with the option to set text RGB colour. 29 | #### Add Link 30 | This action inserts a hyperlink from the input text and URL, into a selected column of the selected rows at either the end or the beginning. 31 | #### Add Attachment 32 | This action inserts attachments into a selected column of the selected row, picked from `Files.app`. The attachments can be inserted as files or URLs. 33 | 34 | #### Rename Attachment 35 | This action renames all attachments in the selected rows, allowing automatic renaming based on column texts, e.g. `{%Topic}-[{%Notes}]-({%Status})`. It allows renaming all at once or manually review and confirm for each attachment. 36 | #### Duplicate Column 37 | This action duplicates a selected column and its contents to a new column to the right. 38 | ### Format 39 | #### Paste Style 40 | This action pastes the style of the selected row into selected targets, including all rows, children, descendants, leaves, parent, ancestors, and preceding/following (collateral) siblings. Each target option is a toggle so multiple selections are allowed. 41 | #### Clear Style 42 | This action sets the style of the selected rows and all their texts to the document base style. 43 | #### Line Spacing 44 | This action adjusts the base line spacing of the document, relative to base font size. 45 | #### Column Formatter 46 | This action changes the column formatter for a selected column. It exposes all column formatting options for date, duration, and number columns, with options of any calendars, time zones, and currencies unavailable through native interface. 47 | #### Apply Title Case 48 | This action sets the texts in a selected column from the selected rows to title case. 49 | #### Split Paragraph 50 | This action splits the selected rows according to the paragraphs in a selected column. It includes all descendants to the new rows. 51 | #### Trim Column Title 52 | This action removes trailing white spaces in all column titles from both ends. 53 | ### View 54 | #### Focus 55 | This action sets the focus of the editor to the selected rows. 56 | #### Hide Column 57 | This action hides all additional columns. 58 | #### Word Count 59 | This action shows the count of words in a selected text column of the selected rows. 60 | #### Character Count 61 | This action shows the count of characters in a selected text column of the selected rows. 62 | #### Column Statistics 63 | This action shows the basic statistics of a selected number or duration column of the selected rows, including sample size, sum, mean, standard deviation, maximum, minimum, and median. 64 | #### Render LaTeX 65 | This action presents in share sheet a base 64 encoded url of a html file to render LaTeX maths formulae to svg or png using mathjax 3. The url needs be opened in a web browser either manually, or automated using Shortcuts, or Scriptable. 66 | ### Share 67 | #### Add to Anki 68 | This action sends the selected rows into creating new notes in Anki Mobile. It requires appropriate configuration for card types in Anki, with 3 custom card types defined with custom fields: {Basic: Front, Back, Reference, Reverse, Extra}, {Cloze: Text, Reference, Extra}, {Input: Front, Back, Reference, Extra}. It also requires corresponding columns being present in the current document. Otherwise it prompts to create a template document. 69 | #### Add to Things 70 | This action sends the selected rows into creating/updating tasks/projects in Things. It includes the OmniOutliner row link in the notes, and has the option to send the Things URL back. Which column are sent to task/project title and notes can be changed. There are toggle options to include checklist, when, reminder, deadline, and tags. 71 | #### Add to DEVONthink 72 | This action sends the selected rows into creating new text documents in DEVONthink. It sends the document title as title, topic as body, notes as notes, and row link as URL. 73 | #### Share Column 74 | This action presents the contents of a selected column of the selected rows in share sheet as plain texts, with the option of sharing as an array of texts. 75 | #### Share Attachment 76 | This action presents the attachments in the selected rows in share sheet. 77 | #### Share as Link 78 | This action presents the row links of the selected rows in share sheet, e.g. `omnioutliner:///open?row=fUpE2aoNbcL`, with the option of sharing as an array of links. 79 | #### Share as Markdown 80 | This action presents a `.md` file generated from the texts of a selected text column of the selected rows in share sheet. If there’re ‘#’ existing in the texts, it only counts those rows as header and automatically adds more ‘#’ depending on indent level. Otherwise it assumes all rows as headers and add ‘#’ to all of them depending on indent level. 81 | #### Share Clipboard 82 | This action presents the contents in clipboard in share sheet. 83 | ### BibTeX 84 | #### Copy Cite Key 85 | This action converts the texts in the ‘EntryKey’ column of the selected rows into LaTeX citation command `\cite`, and copies it into the clipboard when confirmed. 86 | #### Copy Bibliography 87 | This action converts the texts in the ‘title’, ‘author’, ‘year’ columns of the selected rows into LaTeX bibliography command `\bibitem`, and copies it into the clipboard when confirmed. 88 | #### Unique Key 89 | This action changes duplicate texts in the ‘EntryKey’ column to ensure all cite keys are unique. 90 | #### Import Attachment 91 | This action adds attachments either as files or URL links into a selected column picked from `Files.app`. It can automatically add the attachment into its corresponding row, by matching its filename with texts in visible columns. Optionally the failed matches can be added as new rows. 92 | #### Import BibTeX 93 | This action imports a `.bib` file picked from `Files.app`, or appreciate texts from clipboard, into the current document. It creates a new column for each field if necessary. It only shows important columns to prevent performance issues when too many columns are visible. This action is made possible by the [bib2json](https://github.com/mayanklahiri/bib2json) project owned by Mayank Lahiri. 94 | #### Export BibTeX 95 | This action presents a `.bib` file generated from the selected rows in share sheet. -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/ApplicationLib.js: -------------------------------------------------------------------------------- 1 | var _ = (function () { 2 | var ApplicationLib = new PlugIn.Library(new Version("1.0")); 3 | 4 | ApplicationLib.isBeforeCurVers = function (versStrToCheck) { 5 | curVersStr = app.version; 6 | curVers = new Version(curVersStr); 7 | versToCheck = new Version(versStrToCheck); 8 | result = versToCheck.isBefore(curVers); 9 | console.log( 10 | versStrToCheck + " is before " + curVersStr + " = " + result 11 | ); 12 | return result; 13 | }; 14 | 15 | ApplicationLib.isEqualToCurVers = function (versStrToCheck) { 16 | curVersStr = app.version; 17 | curVers = new Version(curVersStr); 18 | versToCheck = new Version(versStrToCheck); 19 | result = versToCheck.equals(curVers); 20 | console.log(versStrToCheck + " equals " + curVersStr + " = " + result); 21 | return result; 22 | }; 23 | 24 | ApplicationLib.isAtLeastCurVers = function (versStrToCheck) { 25 | curVersStr = app.version; 26 | curVers = new Version(curVersStr); 27 | versToCheck = new Version(versStrToCheck); 28 | result = versToCheck.atLeast(curVers); 29 | console.log( 30 | versStrToCheck + " is at least " + curVersStr + " = " + result 31 | ); 32 | return result; 33 | }; 34 | 35 | ApplicationLib.isAfterCurVers = function (versStrToCheck) { 36 | curVersStr = app.version; 37 | curVers = new Version(curVersStr); 38 | versToCheck = new Version(versStrToCheck); 39 | result = versToCheck.isAfter(curVers); 40 | console.log( 41 | versStrToCheck + " is after " + curVersStr + " = " + result 42 | ); 43 | return result; 44 | }; 45 | 46 | // returns a list of functions 47 | ApplicationLib.handlers = function handlers() { 48 | return "\n// ApplicationLib ©2017 The PlugIn Author\n• isBeforeCurVers(versStrToCheck)\n• isEqualToCurVers(versStrToCheck)\n• isAtLeastCurVers(versStrToCheck)\n• isAfterCurVers(versStrToCheck)"; 49 | }; 50 | 51 | // returns contents of matching strings file 52 | ApplicationLib.documentation = function () { 53 | // create a version object 54 | var aVersion = new Version("1.0"); 55 | // look up the plugin 56 | var plugin = PlugIn.find("com.PlugInAuthorID.OmniOutliner", aVersion); 57 | // get the url for the text file inside this plugin 58 | var url = plugin.resourceNamed("ApplicationLib.strings"); 59 | // read the file 60 | url.fetch(function (data) { 61 | dataString = data.toString(); 62 | console.log(dataString); // show in console 63 | return dataString; 64 | }); 65 | }; 66 | 67 | return ApplicationLib; 68 | })(); 69 | _; 70 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/ApplicationLib.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2017 The PlugIn Author 3 | • isBeforeCurVers(versStrToCheck) // checks whether the provided version string comes before the current application version 4 | • isEqualToCurVers(versStrToCheck) // checks whether the provided version string is equal to the current application version 5 | • isAtLeastCurVers(versStrToCheck) // checks whether the provided version string is at least the current application version 6 | • isAfterCurVers(versStrToCheck) // checks whether the provided version string comes after the current application version -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/Parser.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2021 Taxyovio 3 | • BibtexParser(arg0) // A forgiving Bibtex to JSON parser -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/copyBIBITEM.js: -------------------------------------------------------------------------------- 1 | // This action copies the entry key(s) for the selected row(s) into ~\cite{key1, key2, key3, ...}. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | const editor = document.editors[0]; 8 | 9 | if (!document.outline.columns.byTitle("EntryKey")) { 10 | throw new Error("No column: EntryKey."); 11 | } else if ( 12 | document.outline.columns.byTitle("EntryKey").type !== 13 | Column.Type.Text 14 | ) { 15 | throw new Error("No text column: EntryKey."); 16 | } 17 | if (!document.outline.columns.byTitle("title")) { 18 | throw new Error("No column: title."); 19 | } else if ( 20 | document.outline.columns.byTitle("title").type !== Column.Type.Text 21 | ) { 22 | throw new Error("No text column: title."); 23 | } 24 | if (!document.outline.columns.byTitle("author")) { 25 | throw new Error("No column: author."); 26 | } else if ( 27 | document.outline.columns.byTitle("author").type !== Column.Type.Text 28 | ) { 29 | throw new Error("No text column: author."); 30 | } 31 | if (!document.outline.columns.byTitle("year")) { 32 | throw new Error("No column: year."); 33 | } else if ( 34 | document.outline.columns.byTitle("year").type !== Column.Type.Text 35 | ) { 36 | throw new Error("No text column: year."); 37 | } 38 | 39 | // All text columns titled 'EntryKey' 40 | var keyColumns = document.outline.columns.filter(function (column) { 41 | if ( 42 | column.type === Column.Type.Text && 43 | column.title === "EntryKey" 44 | ) { 45 | return column; 46 | } 47 | }); 48 | // Take the first one as the source column 49 | var keyColumn = keyColumns[0]; 50 | 51 | var titleColumns = document.outline.columns.filter(function (column) { 52 | if (column.type === Column.Type.Text && column.title === "title") { 53 | return column; 54 | } 55 | }); 56 | var titleColumn = titleColumns[0]; 57 | 58 | var authorColumns = document.outline.columns.filter(function (column) { 59 | if (column.type === Column.Type.Text && column.title === "author") { 60 | return column; 61 | } 62 | }); 63 | var authorColumn = authorColumns[0]; 64 | 65 | var yearColumns = document.outline.columns.filter(function (column) { 66 | if (column.type === Column.Type.Text && column.title === "year") { 67 | return column; 68 | } 69 | }); 70 | var yearColumn = yearColumns[0]; 71 | 72 | // \bibitem{hori} {K.~Hori, S.~Katz, A.~Klemm, R.~Pandharipande, R.~Thomas, C.~Vafa, R.~Vakil and E.~Zaslow}, \textit{Mirror Symmetry}, AMS, Providence, USA (2003). 73 | 74 | var bibitems = []; 75 | selection.items.forEach((item) => { 76 | var entryKeyText = item.valueForColumn(keyColumn); 77 | if (entryKeyText && entryKeyText.string.trim().length !== 0) { 78 | var bibitem = { 79 | key: entryKeyText.string.trim(), 80 | author: "", 81 | title: "", 82 | year: "", 83 | }; 84 | 85 | var authorText = item.valueForColumn(authorColumn); 86 | if (authorText && authorText.string.trim().length !== 0) { 87 | authors = authorText.string.trim().split(" and "); 88 | authors.forEach((author, i) => { 89 | if (i !== authors.length - 1) { 90 | bibitem.author += author + ", "; 91 | } else if (authors.length === 1) { 92 | bibitem.author += author; 93 | } else { 94 | bibitem.author += "and " + author; 95 | } 96 | }); 97 | } 98 | 99 | var titleText = item.valueForColumn(titleColumn); 100 | if (titleText && titleText.string.trim().length !== 0) { 101 | bibitem.title = titleText.string.trim(); 102 | } 103 | 104 | var yearText = item.valueForColumn(yearColumn); 105 | if (yearText && yearText.string.trim().length !== 0) { 106 | bibitem.year = yearText.string.trim(); 107 | } 108 | bibitems.push(bibitem); 109 | } 110 | }); 111 | 112 | var str = ""; 113 | bibitems.forEach((bibitem) => { 114 | str += "\\bibitem{" + bibitem.key + "} "; 115 | str += "{" + bibitem.author + "}, "; 116 | str += "\\textit{" + bibitem.title + "}, "; 117 | str += "(" + bibitem.year + ").\n"; 118 | }); 119 | 120 | var alertTitle = "BIBITEM"; 121 | var alertMessage = str; 122 | 123 | var alert = new Alert(alertTitle, alertMessage); 124 | alert.addOption("Cancel"); 125 | alert.addOption("Copy"); 126 | var alertPromise = alert.show(); 127 | 128 | alertPromise.then((buttonIndex) => { 129 | if (buttonIndex === 1) { 130 | console.log("Continue script"); 131 | Pasteboard.general.string = str; 132 | } else { 133 | throw new Error("script cancelled"); 134 | } 135 | }); 136 | }); 137 | 138 | action.validate = function (selection, sender) { 139 | // validation code 140 | // selection options: columns, document, editor, items, nodes, styles 141 | if (selection.items.length > 0) { 142 | return true; 143 | } else { 144 | return false; 145 | } 146 | }; 147 | 148 | return action; 149 | })(); 150 | _; 151 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/copyKey.js: -------------------------------------------------------------------------------- 1 | // This action copies the entry key(s) for the selected row(s) into ~\cite{key1, key2, key3, ...}. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | const editor = document.editors[0]; 8 | 9 | if (!document.outline.columns.byTitle("EntryKey")) { 10 | throw new Error("No column: EntryKey."); 11 | } else if ( 12 | document.outline.columns.byTitle("EntryKey").type !== 13 | Column.Type.Text 14 | ) { 15 | throw new Error("No text column: EntryKey."); 16 | } 17 | 18 | // All text columns titled 'EntryKey' 19 | var keyColumns = document.outline.columns.filter(function (column) { 20 | if ( 21 | column.type === Column.Type.Text && 22 | column.title === "EntryKey" 23 | ) { 24 | return column; 25 | } 26 | }); 27 | // Take the first one as the source column 28 | var keyColumn = keyColumns[0]; 29 | 30 | var str = "~\\cite{"; 31 | var keys = selection.items.map((item) => { 32 | var entryKeyText = item.valueForColumn(keyColumn); 33 | if (entryKeyText && entryKeyText.string.trim().length !== 0) { 34 | return entryKeyText.string.trim(); 35 | } 36 | return null; 37 | }); 38 | 39 | keys = keys.filter((key) => { 40 | return key; 41 | }); 42 | 43 | str += keys.join(", "); 44 | str += "}"; 45 | 46 | var alertTitle = "Cite Key"; 47 | var alertMessage = str; 48 | 49 | var alert = new Alert(alertTitle, alertMessage); 50 | alert.addOption("Cancel"); 51 | alert.addOption("Copy"); 52 | var alertPromise = alert.show(); 53 | 54 | alertPromise.then((buttonIndex) => { 55 | if (buttonIndex === 1) { 56 | console.log("Continue script"); 57 | Pasteboard.general.string = str; 58 | } else { 59 | throw new Error("script cancelled"); 60 | } 61 | }); 62 | }); 63 | 64 | action.validate = function (selection, sender) { 65 | // validation code 66 | // selection options: columns, document, editor, items, nodes, styles 67 | if (selection.items.length > 0) { 68 | return true; 69 | } else { 70 | return false; 71 | } 72 | }; 73 | 74 | return action; 75 | })(); 76 | _; 77 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/copyBIBITEM.strings: -------------------------------------------------------------------------------- 1 | "label" = "Copy Bibliography"; 2 | "shortLabel" = "Bibliography"; 3 | "mediumLabel" = "Copy Bibliography"; 4 | "longLabel" = "Copy bibliography from selected rows"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/copyKey.strings: -------------------------------------------------------------------------------- 1 | "label" = "Copy Cite Key"; 2 | "shortLabel" = "Cite"; 3 | "mediumLabel" = "Copy Key"; 4 | "longLabel" = "Copy entry keys from selected rows"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/exportBibTeX.strings: -------------------------------------------------------------------------------- 1 | "label" = "Export BibTeX"; 2 | "shortLabel" = "Export"; 3 | "mediumLabel" = "Export BibTeX"; 4 | "longLabel" = "Export BibTeX from the document or selected rows"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/importAttachment.strings: -------------------------------------------------------------------------------- 1 | "label" = "Import Attachment"; 2 | "shortLabel" = "Attachment"; 3 | "mediumLabel" = "Import Attachment"; 4 | "longLabel" = "Import attachments from files"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/importBibTeX.strings: -------------------------------------------------------------------------------- 1 | "label" = "Import BibTeX"; 2 | "shortLabel" = "Import"; 3 | "mediumLabel" = "Import BibTeX"; 4 | "longLabel" = "Import BibTeX from clipboard or files"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.taxyovio.bibtex" = "⓹ BibTeX"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/en.lproj/uniqueKey.strings: -------------------------------------------------------------------------------- 1 | "label" = "Unique Key"; 2 | "shortLabel" = "Unique Key"; 3 | "mediumLabel" = "Unique Key"; 4 | "longLabel" = "Make entry keys unique"; -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/exportBibTeX.js: -------------------------------------------------------------------------------- 1 | // This action exports a bib file from the selected rows or the whole document. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection) { 4 | var bibStr = exportBibTeX(selection.items); 5 | var data = Data.fromString(bibStr); 6 | var fileWrapper = FileWrapper.withContents( 7 | document.name + ".bib", 8 | data 9 | ); 10 | sharePanel = new SharePanel([fileWrapper]); 11 | sharePanel.show(); 12 | }); 13 | 14 | // routine determines if menu item is enabled 15 | action.validate = function (selection) { 16 | if (selection.items.length !== 0) { 17 | return true; 18 | } else { 19 | return false; 20 | } 21 | }; 22 | 23 | return action; 24 | })(); 25 | _; 26 | 27 | // This function takes an array of outline items and returns a string of bibtex data. 28 | function exportBibTeX(items) { 29 | const editor = document.editors[0]; 30 | 31 | if (!document.outline.columns.byTitle("EntryKey")) { 32 | throw new Error("No column: EntryKey."); 33 | } else if ( 34 | document.outline.columns.byTitle("EntryKey").type !== Column.Type.Text 35 | ) { 36 | throw new Error("No text column: EntryKey."); 37 | } 38 | 39 | if (!document.outline.columns.byTitle("EntryType")) { 40 | throw new Error("No column: EntryType."); 41 | } else if ( 42 | document.outline.columns.byTitle("EntryType").type !== Column.Type.Text 43 | ) { 44 | throw new Error("No text column: EntryType."); 45 | } 46 | 47 | // List all text columns 48 | var filteredColumns = document.outline.columns.filter(function (column) { 49 | if (column.type === Column.Type.Text) { 50 | return column; 51 | } 52 | }); 53 | 54 | var filteredColumnTitles = filteredColumns.map(function (column) { 55 | if (column.title !== "") { 56 | return column.title; 57 | } else if (column === document.outline.noteColumn) { 58 | // The note column has empty title 59 | return "Notes"; 60 | } 61 | }); 62 | console.log("columns to export: ", filteredColumnTitles); 63 | 64 | var str = ""; 65 | 66 | items.forEach((item) => { 67 | var entryTypeText = item.valueForColumn( 68 | filteredColumns[filteredColumnTitles.indexOf("EntryType")] 69 | ); 70 | var entryKeyText = item.valueForColumn( 71 | filteredColumns[filteredColumnTitles.indexOf("EntryKey")] 72 | ); 73 | 74 | if ( 75 | entryTypeText && 76 | entryTypeText.string.trim().length !== 0 && 77 | entryKeyText && 78 | entryKeyText.string.trim().length !== 0 79 | ) { 80 | // First line of a bibtex entry 81 | str += 82 | "@" + 83 | entryTypeText.string.trim() + 84 | "{" + 85 | entryKeyText.string.trim() + 86 | ",\n"; 87 | 88 | // Export other columns as fields 89 | var fieldColumns = filteredColumns.filter(function (column) { 90 | if ( 91 | column !== 92 | filteredColumns[ 93 | filteredColumnTitles.indexOf("EntryType") 94 | ] && 95 | column !== 96 | filteredColumns[ 97 | filteredColumnTitles.indexOf("EntryKey") 98 | ] 99 | ) { 100 | return column; 101 | } 102 | }); 103 | 104 | fieldColumns.forEach((column) => { 105 | if ( 106 | item.valueForColumn(column) && 107 | item.valueForColumn(column).string.trim().length !== 0 108 | ) { 109 | str += 110 | "\t" + 111 | column.title + 112 | " = {" + 113 | item.valueForColumn(column).string.trim() + 114 | "},\n"; 115 | } 116 | }); 117 | str += "}\n\n"; 118 | } 119 | }); 120 | console.log("exported string:\n", str); 121 | return str; 122 | } 123 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taxyovio/OmniOutliner-Plug-Ins/bca08f8ae5409fcac616217c604b5cfcc5458dd9/bibtex.omnioutlinerjs/Resources/icon.png -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/importBibTeX.js: -------------------------------------------------------------------------------- 1 | // This action imports bibtex entries into rows from clipboard or bib file. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection) { 4 | const Parser = this.plugIn.library("Parser"); 5 | // If clipboard contains bibtex string, import that string. 6 | var pb = Pasteboard.general; 7 | if (pb.hasStrings && /^@/.test(pb.string)) { 8 | var alertTitle = "BibTeX"; 9 | var alertMessage = pb.string; 10 | var alert = new Alert(alertTitle, alertMessage); 11 | alert.addOption("Cancel"); 12 | alert.addOption("Import"); 13 | var alertPromise = alert.show(); 14 | 15 | alertPromise.then((buttonIndex) => { 16 | if (buttonIndex === 1) { 17 | console.log("Continue script"); 18 | var bibStr = pb.string; 19 | bibStr = escapeBibStr(bibStr); // Escape special chars \ and { } 20 | console.log("bibStr:\n", bibStr); 21 | importBibTeX(bibStr, Parser); 22 | } else { 23 | throw new Error("Script cancelled"); 24 | } 25 | }); 26 | } else { 27 | console.log("Clipboard has no bibtex."); 28 | var bibtexType = new TypeIdentifier("org.tug.tex.bibtex"); 29 | var picker = new FilePicker(); 30 | picker.folders = false; 31 | picker.multiple = true; 32 | picker.types = [bibtexType]; 33 | 34 | pickerPromise = picker.show(); 35 | 36 | // PROMISE FUNCTION CALLED UPON PICKER APPROVAL 37 | pickerPromise.then(function (urlsArray) { 38 | urlsArray.forEach((url) => { 39 | var filename = decodeURIComponent( 40 | url.string.substring(url.string.lastIndexOf("/") + 1) 41 | ); 42 | console.log("Importing: ", filename); 43 | url.fetch(function (data) { 44 | var bibStr = data.toString(); 45 | //bibStr = escapeBibStr(bibStr) // Escape special chars \ and { } 46 | importBibTeX(bibStr, Parser); 47 | }); 48 | }); 49 | }); 50 | 51 | // PROMISE FUNCTION CALLED UPON PICKER CANCELLATION 52 | pickerPromise.catch(function (error) { 53 | console.log("form cancelled", error.message); 54 | }); 55 | } 56 | }); 57 | 58 | // routine determines if menu item is enabled 59 | action.validate = function (selection) { 60 | if (document && selection.items.length <= 1) { 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | }; 66 | 67 | return action; 68 | })(); 69 | _; 70 | 71 | // This function takes a string bibStr containing bibtex and add each entry to a new row, using the bib2JSON Parser. 72 | function importBibTeX(bibStr, Parser) { 73 | // List all text columns 74 | const editor = document.editors[0]; 75 | var filteredColumns = columns.filter(function (column) { 76 | if (column.type === Column.Type.Text) { 77 | return column; 78 | } 79 | }); 80 | 81 | var filteredColumnTitles = filteredColumns.map(function (column) { 82 | if (column.title !== "") { 83 | return column.title; 84 | } else if (column === document.outline.noteColumn) { 85 | // The note column has empty title for unknown reason 86 | return "Notes"; 87 | } 88 | }); 89 | 90 | var bibJSON = Parser.BibtexParser(bibStr); 91 | // {"entries":[{"ObjectType":"entry","EntryType":"book","EntryKey":"book1","Fields":{"author":"Donald Knuth","title":"Concrete Mathematics"}}],"errors":[]} 92 | var entries = bibJSON.entries; 93 | console.log( 94 | "Parsed JSON with", 95 | entries.length, 96 | "entries:\n", 97 | JSON.stringify(bibJSON), 98 | "\nerrors:\n", 99 | bibJSON.errors 100 | ); 101 | var columnTitles = filteredColumnTitles; //['EntryType', 'EntryKey', 'title'] 102 | 103 | if (!entries) { 104 | throw new Error("No BibTeX entries are found."); 105 | } 106 | 107 | // Create a list of columns 108 | const outline = document.outline; 109 | entries.forEach((entry) => { 110 | if (columnTitles.indexOf("EntryKey") === -1) { 111 | columnTitles.push("EntryKey"); 112 | outline.addColumn( 113 | Column.Type.Text, 114 | editor.beforeColumn(outlineColumn), 115 | function (column) { 116 | column.title = "EntryKey"; 117 | } 118 | ); 119 | } 120 | 121 | if (columnTitles.indexOf("EntryType") === -1) { 122 | columnTitles.push("EntryType"); 123 | outline.addColumn( 124 | Column.Type.Text, 125 | editor.beforeColumn(outlineColumn), 126 | function (column) { 127 | column.title = "EntryType"; 128 | } 129 | ); 130 | } 131 | 132 | var fields = entry.Fields; 133 | Object.getOwnPropertyNames(fields).forEach((fieldName) => { 134 | if (columnTitles.indexOf(fieldName.toLowerCase()) === -1) { 135 | columnTitles.push(fieldName.toLowerCase()); 136 | outline.addColumn( 137 | Column.Type.Text, 138 | editor.afterColumn(), 139 | function (column) { 140 | column.title = fieldName.toLowerCase(); 141 | if (!isEssentialField(fieldName)) { 142 | editor.setVisibilityOfColumn(column, false); 143 | } 144 | } 145 | ); 146 | } 147 | }); 148 | }); 149 | console.log("columns:", columnTitles); 150 | 151 | // Add entries into rows 152 | entries.forEach((entry) => { 153 | var fields = entry.Fields; 154 | if (editor.selection.items.length === 1) { 155 | var position = editor.selection.items[0].after; 156 | var item = editor.selection.items[0].parent; 157 | } else { 158 | var position = null; 159 | var item = outline.rootItem; 160 | } 161 | 162 | item.addChild(position, function (item) { 163 | item.setValueForColumn( 164 | entry.EntryType, 165 | outline.columns.byTitle("EntryType") 166 | ); 167 | item.setValueForColumn( 168 | entry.EntryKey, 169 | outline.columns.byTitle("EntryKey") 170 | ); 171 | Object.getOwnPropertyNames(fields).forEach((fieldName) => { 172 | if (fields[fieldName] && fields[fieldName].trim()) { 173 | var fieldValue = fields[fieldName].trim(); 174 | var textObj = new Text(fieldValue, item.style); 175 | if (fieldName.toLowerCase() === "doi") { 176 | var url = URL.fromString( 177 | "https://doi.org/" + fieldValue 178 | ); 179 | textObj.style.set(Style.Attribute.Link, url); 180 | } 181 | var textColumns = columns.filter(function (column) { 182 | if (column.type === Column.Type.Text) { 183 | return column; 184 | } 185 | }); 186 | item.setValueForColumn( 187 | textObj, 188 | columnByTitle(textColumns, fieldName.toLowerCase()) 189 | ); 190 | } 191 | }); 192 | }); 193 | }); 194 | var alertTitle = "Confirmation"; 195 | if (entries.length === 0) { 196 | if (bibJSON.errors.length === 0) { 197 | var alertMessage = 198 | "No bibtex entry has been imported, with no error."; 199 | } else if (bibJSON.errors.length === 1) { 200 | var alertMessage = 201 | "No bibtex entry has been imported, with 1 error."; 202 | } else { 203 | var alertMessage = 204 | "No bibtex entry has been imported, with " + 205 | bibJSON.errors.length + 206 | " errors."; 207 | } 208 | } else if (entries.length === 1) { 209 | if (bibJSON.errors.length === 0) { 210 | var alertMessage = 211 | "1 bibtex entry has been imported, with no error."; 212 | } else if (bibJSON.errors.length === 1) { 213 | var alertMessage = 214 | "1 bibtex entry has been imported, with 1 error."; 215 | } else { 216 | var alertMessage = 217 | "1 bibtex entry has been imported, with " + 218 | bibJSON.errors.length + 219 | " errors."; 220 | } 221 | } else { 222 | if (bibJSON.errors.length === 0) { 223 | var alertMessage = 224 | entries.length + 225 | " bibtex entries have been imported, with no error."; 226 | } else if (bibJSON.errors.length === 1) { 227 | var alertMessage = 228 | entries.length + 229 | " bibtex entries have been imported, with 1 error."; 230 | } else { 231 | var alertMessage = 232 | entries.length + 233 | " bibtex entries have been imported, with " + 234 | bibJSON.errors.length + 235 | " errors."; 236 | } 237 | } 238 | 239 | var alert = new Alert(alertTitle, alertMessage); 240 | alert.show(); 241 | return bibJSON; 242 | } 243 | 244 | function reverse(str) { 245 | return str.split("").reverse().join(""); 246 | } 247 | 248 | function escapeBibStr(bibStr) { 249 | // Add new line char if the last char is not to help regex 250 | if (bibStr.slice(-1) !== "\n") { 251 | bibStr += "\n"; 252 | } 253 | 254 | bibStr = bibStr.replace(/\\/g, "\\\\"); // Escape backslash 255 | bibStr = bibStr.replace(/\}(?!(\s*,\s*\n)|\s*\n|\s*$)/g, "\\}"); // Escape { 256 | 257 | // Escape } by reversing the str first as js doesn't have lookbehind 258 | var reverseStr = reverse(bibStr); 259 | reverseStr = reverseStr.replace(/\{(?!(\w*@$)|(\s*=\s*\w*\s*\n,))/g, "{\\"); 260 | bibStr = reverse(reverseStr); 261 | return bibStr; 262 | } 263 | 264 | // Return if a field is essential to determine new column visibility 265 | function isEssentialField(filedName) { 266 | filedName = filedName.toLocaleLowerCase(); 267 | const essentialTitles = [ 268 | "title", 269 | "author", 270 | "year", 271 | "eprint", 272 | "journal", 273 | "doi", 274 | "publisher", 275 | "url", 276 | "keywords", 277 | "abstract", 278 | "date", 279 | "booktitle", 280 | "owner", 281 | "timestamp", 282 | ]; 283 | if (essentialTitles.indexOf(filedName) !== -1) { 284 | return true; 285 | } else { 286 | return false; 287 | } 288 | } 289 | 290 | function columnByTitle(columnArray, title) { 291 | for (var i = 0; i < columnArray.length; i++) { 292 | if (columnArray[i].title === title) { 293 | return columnArray[i]; 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/Resources/uniqueKey.js: -------------------------------------------------------------------------------- 1 | // This action renames duplicate entry keys to make them unique. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection) { 4 | editor = document.editors[0]; 5 | if (!document.outline.columns.byTitle("EntryKey")) { 6 | throw new Error("No column: EntryKey."); 7 | } else if ( 8 | document.outline.columns.byTitle("EntryKey").type !== 9 | Column.Type.Text 10 | ) { 11 | throw new Error("No text column: EntryKey."); 12 | } 13 | 14 | if (!document.outline.columns.byTitle("EntryType")) { 15 | throw new Error("No column: EntryType."); 16 | } else if ( 17 | document.outline.columns.byTitle("EntryType").type !== 18 | Column.Type.Text 19 | ) { 20 | throw new Error("No text column: EntryType."); 21 | } 22 | 23 | // Record the row items and their EntryKey strings. 24 | var keysObj = { keys: [], items: [] }; 25 | 26 | rootItem.descendants.forEach((item, index) => { 27 | var entryKeyText = item.valueForColumn( 28 | document.outline.columns.byTitle("EntryKey") 29 | ); 30 | if (entryKeyText && entryKeyText.string.length !== 0) { 31 | keysObj.keys.push(entryKeyText.string); 32 | keysObj.items.push(item); 33 | } else { 34 | var entryTypeText = item.valueForColumn( 35 | document.outline.columns.byTitle("EntryType") 36 | ); 37 | if (entryTypeText && entryTypeText.string.length !== 0) { 38 | if (entryKeyText) { 39 | entryKeyText.string = 40 | entryTypeText.string.trim() + index; 41 | } else { 42 | var text = new Text( 43 | entryTypeText.string.trim() + index, 44 | item.style 45 | ); 46 | item.setValueForColumn( 47 | text, 48 | document.outline.columns.byTitle("EntryKey") 49 | ); 50 | } 51 | } else { 52 | if (entryKeyText) { 53 | entryKeyText.string = index.toString(); 54 | } else { 55 | var text = new Text(index.toString(), item.style); 56 | item.setValueForColumn( 57 | text, 58 | document.outline.columns.byTitle("EntryKey") 59 | ); 60 | } 61 | } 62 | entryKeyText = item.valueForColumn( 63 | document.outline.columns.byTitle("EntryKey") 64 | ); 65 | keysObj.keys.push(entryKeyText.string); 66 | keysObj.items.push(item); 67 | } 68 | }); 69 | 70 | if (hasDuplicates(keysObj.keys)) { 71 | var duplicateCount = 72 | keysObj.keys.length - keysObj.keys.filter(onlyUnique).length; 73 | 74 | var alertTitle = "Confirmation"; 75 | if (duplicateCount === 1) { 76 | var alertMessage = "Rename 1 duplicate entry key?"; 77 | } else { 78 | var alertMessage = 79 | "Rename " + duplicateCount + " duplicate entry keys?"; 80 | } 81 | var alert = new Alert(alertTitle, alertMessage); 82 | alert.addOption("Cancel"); 83 | alert.addOption("Continue"); 84 | var alertPromise = alert.show(); 85 | 86 | alertPromise.then((buttonIndex) => { 87 | if (buttonIndex === 1) { 88 | console.log("Continue script"); 89 | 90 | var uniqueKeys = renameStrings(keysObj.keys); 91 | var renamingLog = ""; 92 | uniqueKeys.forEach((key, i) => { 93 | var item = keysObj.items[i]; 94 | var ogKeyText = item.valueForColumn( 95 | document.outline.columns.byTitle("EntryKey") 96 | ); 97 | if (ogKeyText.string !== key) { 98 | renamingLog += 99 | ogKeyText.string + " -> " + key + "\n"; 100 | ogKeyText.string = key; 101 | } 102 | }); 103 | 104 | var alertTitle = "Confirmation"; 105 | var alertMessage = renamingLog; 106 | var alert = new Alert(alertTitle, alertMessage); 107 | alert.show(); 108 | } else { 109 | throw new Error("script cancelled"); 110 | } 111 | }); 112 | } else { 113 | var alertTitle = "Confirmation"; 114 | var alertMessage = "No duplicate entry keys are found."; 115 | var alert = new Alert(alertTitle, alertMessage); 116 | alert.show(); 117 | } 118 | }); 119 | 120 | // routine determines if menu item is enabled 121 | action.validate = function (selection) { 122 | if (document) { 123 | return true; 124 | } else { 125 | return false; 126 | } 127 | }; 128 | 129 | return action; 130 | })(); 131 | _; 132 | 133 | function renameStrings(arr) { 134 | var count = {}; 135 | arr.forEach(function (x, i) { 136 | if (arr.indexOf(x) !== i) { 137 | var c = x in count ? (count[x] = count[x] + 1) : (count[x] = 1); 138 | var j = c + 1; 139 | var k = x + j; 140 | while (arr.indexOf(k) !== -1) k = x + ++j; 141 | arr[i] = k; 142 | } 143 | }); 144 | return arr; 145 | } 146 | 147 | function hasDuplicates(array) { 148 | return new Set(array).size !== array.length; 149 | } 150 | 151 | function onlyUnique(value, index, self) { 152 | return self.indexOf(value) === index; 153 | } 154 | -------------------------------------------------------------------------------- /bibtex.omnioutlinerjs/manifest.json: -------------------------------------------------------------------------------- 1 | {"author":"Taxyovio","libraries":[{"identifier":"ApplicationLib"},{"identifier":"Parser"}],"identifier":"com.taxyovio.bibtex","version":"2021.03.25","description":"A BibTeX reference manager based on Omni Automation in OmniOutliner.","actions":[ 2 | {"image":"key","identifier":"copyKey"}, 3 | {"image":"list.bullet.below.rectangle","identifier":"copyBIBITEM"}, 4 | {"image":"arrow.2.squarepath","identifier":"uniqueKey"}, 5 | {"image":"rectangle.and.paperclip","identifier":"importAttachment"}, 6 | {"image":"tray.and.arrow.down","identifier":"importBibTeX"}, 7 | {"image":"tray.and.arrow.up","identifier":"exportBibTeX"}, 8 | ],"defaultLocale":"en"} -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/ApplicationLib.js: -------------------------------------------------------------------------------- 1 | var _ = (function () { 2 | var ApplicationLib = new PlugIn.Library(new Version("1.0")); 3 | 4 | ApplicationLib.isBeforeCurVers = function (versStrToCheck) { 5 | curVersStr = app.version; 6 | curVers = new Version(curVersStr); 7 | versToCheck = new Version(versStrToCheck); 8 | result = versToCheck.isBefore(curVers); 9 | console.log( 10 | versStrToCheck + " is before " + curVersStr + " = " + result 11 | ); 12 | return result; 13 | }; 14 | 15 | ApplicationLib.isEqualToCurVers = function (versStrToCheck) { 16 | curVersStr = app.version; 17 | curVers = new Version(curVersStr); 18 | versToCheck = new Version(versStrToCheck); 19 | result = versToCheck.equals(curVers); 20 | console.log(versStrToCheck + " equals " + curVersStr + " = " + result); 21 | return result; 22 | }; 23 | 24 | ApplicationLib.isAtLeastCurVers = function (versStrToCheck) { 25 | curVersStr = app.version; 26 | curVers = new Version(curVersStr); 27 | versToCheck = new Version(versStrToCheck); 28 | result = versToCheck.atLeast(curVers); 29 | console.log( 30 | versStrToCheck + " is at least " + curVersStr + " = " + result 31 | ); 32 | return result; 33 | }; 34 | 35 | ApplicationLib.isAfterCurVers = function (versStrToCheck) { 36 | curVersStr = app.version; 37 | curVers = new Version(curVersStr); 38 | versToCheck = new Version(versStrToCheck); 39 | result = versToCheck.isAfter(curVers); 40 | console.log( 41 | versStrToCheck + " is after " + curVersStr + " = " + result 42 | ); 43 | return result; 44 | }; 45 | 46 | // returns a list of functions 47 | ApplicationLib.handlers = function () { 48 | return "\n// ApplicationLib ©2021 Taxyovio\n• isBeforeCurVers(versStrToCheck)\n• isEqualToCurVers(versStrToCheck)\n• isAtLeastCurVers(versStrToCheck)\n• isAfterCurVers(versStrToCheck)"; 49 | }; 50 | 51 | // returns contents of matching strings file 52 | ApplicationLib.documentation = function () { 53 | // create a version object 54 | var aVersion = new Version("1.0"); 55 | // look up the plugin 56 | var plugin = PlugIn.find("com.Taxyovio.OmniOutliner", aVersion); 57 | // get the url for the text file inside this plugin 58 | var url = plugin.resourceNamed("ApplicationLib.strings"); 59 | // read the file 60 | url.fetch(function (data) { 61 | dataString = data.toString(); 62 | console.log(dataString); // show in console 63 | return dataString; 64 | }); 65 | }; 66 | 67 | // text object to markdown 68 | ApplicationLib.textToMD = function (textObj) { 69 | var str = ""; 70 | var runs = textObj.attributeRuns; 71 | var hasAttachment = !(textObj.attachments.length === 0); 72 | 73 | const imgExts = ["png", "jpeg", "jpg", "bmp", "tiff"]; 74 | 75 | runs.forEach((text) => { 76 | if (text.style.link) { 77 | // Check for hyperlink 78 | var urlStr = text.style.link.string; 79 | 80 | // Check for possible images 81 | var filename = urlStr.substr(urlStr.lastIndexOf("/") + 1); 82 | 83 | if (filename) { 84 | var baseName = filename.substring( 85 | 0, 86 | filename.lastIndexOf(".") 87 | ); 88 | var extension = filename.substring( 89 | filename.lastIndexOf(".") + 1, 90 | filename.length 91 | ); 92 | // If there's no extension in the filename, the above codes assign the whole name to extension. 93 | if (baseName === "") { 94 | baseName = extension; 95 | extension = ""; 96 | } 97 | 98 | if (imgExts.indexOf(extension) !== -1) { 99 | str += "![" + baseName + "](" + urlStr + ")"; 100 | } else { 101 | if (urlStr === text.string) { 102 | str += "<" + urlStr + ">"; 103 | } else { 104 | str += "[" + text.string + "](" + urlStr + ")"; 105 | } 106 | } 107 | } else { 108 | console.log("hi"); 109 | if (urlStr === text.string) { 110 | str += "<" + urlStr + ">"; 111 | } else { 112 | str += "[" + text.string + "](" + urlStr + ")"; 113 | } 114 | } 115 | } else if (text.fileWrapper) { 116 | // Check for attachments and if they are images 117 | var filename = text.fileWrapper.preferredFilename; 118 | var baseName = filename.substring(0, filename.lastIndexOf(".")); 119 | var extension = filename.substring( 120 | filename.lastIndexOf(".") + 1, 121 | filename.length 122 | ); 123 | 124 | // If there's no extension in the filename, the above codes assign the whole name to extension. 125 | if (baseName === "") { 126 | baseName = extension; 127 | extension = ""; 128 | } 129 | 130 | if (imgExts.indexOf(extension) !== -1) { 131 | str += " ![" + baseName + "](" + filename + ") "; 132 | } else { 133 | str += " <" + filename + "> "; 134 | } 135 | } else { 136 | str += text.string; 137 | } 138 | }); 139 | console.log(textObj.string, "->\n", str); 140 | return str; 141 | }; 142 | return ApplicationLib; 143 | })(); 144 | _; 145 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/ApplicationLib.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2021 Taxyovio 3 | • isBeforeCurVers(versStrToCheck) // checks whether the provided version string comes before the current application version 4 | • isEqualToCurVers(versStrToCheck) // checks whether the provided version string is equal to the current application version 5 | • isAtLeastCurVers(versStrToCheck) // checks whether the provided version string is at least the current application version 6 | • isAfterCurVers(versStrToCheck) // checks whether the provided version string comes after the current application version 7 | • textToMD(textObj) // converts the text object into markdown -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/addAttachment.js: -------------------------------------------------------------------------------- 1 | // This action inserts texts at the end or start of the selected column of selected rows. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | 7 | const sizeLimit = 250000000; // The app tends to crahs on iOS/iPadOS with files bigger than 250MB 8 | 9 | var selectedItem = selection.items[0]; 10 | 11 | // List all visible text columns for insertion 12 | editor = document.editors[0]; 13 | var filteredColumns = columns.filter(function (column) { 14 | if (editor.visibilityOfColumn(column)) { 15 | if (column.type === Column.Type.Text) { 16 | return column; 17 | } 18 | } 19 | }); 20 | 21 | if (filteredColumns.length === 0) { 22 | throw new Error("This document has no text columns."); 23 | } 24 | 25 | filteredColumnTitles = filteredColumns.map(function (column) { 26 | if (column.title !== "") { 27 | return column.title; 28 | } else if (column === document.outline.noteColumn) { 29 | // The note column has empty title for unknown reason 30 | return "Notes"; 31 | } 32 | }); 33 | 34 | // CREATE FORM FOR GATHERING USER INPUT 35 | var inputForm = new Form(); 36 | 37 | if (filteredColumns.includes(document.outline.outlineColumn)) { 38 | var defaultColumn = document.outline.outlineColumn; 39 | } else { 40 | var defaultColumn = document.outline.noteColumn; 41 | } 42 | 43 | var columnField = new Form.Field.Option( 44 | "columnInput", 45 | "Column", 46 | filteredColumns, 47 | filteredColumnTitles, 48 | defaultColumn 49 | ); 50 | 51 | // Toggle if import urls or files 52 | var importURLOptionField = new Form.Field.Checkbox( 53 | "importURLOptionInput", 54 | "Add as URL", 55 | false 56 | ); 57 | 58 | // ADD THE FIELDS TO THE FORM 59 | inputForm.addField(columnField); 60 | inputForm.addField(importURLOptionField); 61 | // PRESENT THE FORM TO THE USER 62 | formPrompt = "Select Column and Import Option"; 63 | formPromise = inputForm.show(formPrompt, "Continue"); 64 | 65 | // VALIDATE THE USER INPUT 66 | inputForm.validate = function (formObject) { 67 | return null; 68 | }; 69 | 70 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 71 | formPromise.then(function (formObject) { 72 | var selectedColumn = formObject.values["columnInput"]; 73 | var importURL = formObject.values["importURLOptionInput"]; 74 | 75 | var picker = new FilePicker(); 76 | picker.folders = false; 77 | picker.multiple = true; 78 | picker.types = null; 79 | 80 | pickerPromise = picker.show(); 81 | 82 | // PROMISE FUNCTION CALLED UPON PICKER APPROVAL 83 | pickerPromise.then(function (urlsArray) { 84 | urlsArray.forEach((url, index) => { 85 | urlStr = url.string; 86 | var ogText = selectedItem.valueForColumn(selectedColumn); 87 | 88 | if (importURL) { 89 | if (ogText) { 90 | var textObj = new Text( 91 | urlScheme(urlStr), 92 | ogText.style 93 | ); 94 | if (!ogText.string.slice(-1).match(/\s/)) { 95 | var space = new Text(" ", ogText.style); 96 | ogText.append(space); 97 | ogText.append(textObj); 98 | } 99 | } else { 100 | var textObj = new Text( 101 | urlScheme(urlStr), 102 | selectedItem.style 103 | ); 104 | selectedItem.setValueForColumn( 105 | textObj, 106 | selectedColumn 107 | ); 108 | } 109 | } else { 110 | // GET FILE NAME 111 | var filename = urlStr.substring( 112 | urlStr.lastIndexOf("/") + 1 113 | ); 114 | // REMOVE FILE EXTENSION AND ENCODING 115 | var baseName = filename.substring( 116 | 0, 117 | filename.lastIndexOf(".") 118 | ); 119 | baseName = decodeURIComponent(baseName); 120 | var extension = filename.substring( 121 | filename.lastIndexOf(".") + 1, 122 | filename.length 123 | ); 124 | 125 | // If there's no extension in the filename, the above codes assign the whole name to extension. 126 | if (baseName === "") { 127 | baseName = extension; 128 | extension = ""; 129 | } 130 | if (extension === "") { 131 | filename = baseName; 132 | } else { 133 | filename = baseName + "." + extension; 134 | } 135 | // IMPORT FILES 136 | url.fetch(function (data) { 137 | var size = data.length; 138 | console.log(filename, size, "bytes"); 139 | if ( 140 | size > sizeLimit && 141 | (app.platformName === "iOS" || 142 | app.platformName === "iPadOS") 143 | ) { 144 | const displayNameLimit = 21; 145 | if (filename.length > displayNameLimit) { 146 | var displayName = 147 | baseName.substring( 148 | 0, 149 | baseName.length - 150 | (filename.length - 151 | displayNameLimit) 152 | ) + 153 | "...." + 154 | extension; 155 | } else { 156 | var displayName = filename; 157 | } 158 | var alertTitle = "Confirmation"; 159 | var alertMessage = 160 | displayName + 161 | "\nThis file is larger than 250MB, which may cause crash."; 162 | var alert = new Alert(alertTitle, alertMessage); 163 | 164 | alert.addOption("Skip"); 165 | alert.addOption("Add URL"); 166 | alert.addOption("Add File"); 167 | var alertPromise = alert.show(); 168 | 169 | alertPromise.then((buttonIndex) => { 170 | if (buttonIndex === 0) { 171 | console.log("skip large file"); 172 | } else if (buttonIndex === 1) { 173 | console.log( 174 | "adding url for large file" 175 | ); 176 | if (ogText) { 177 | var textObj = new Text( 178 | urlScheme(urlStr, false), 179 | ogText.style 180 | ); 181 | if ( 182 | !ogText.string 183 | .slice(-1) 184 | .match(/\s/) 185 | ) { 186 | var space = new Text( 187 | " ", 188 | ogText.style 189 | ); 190 | ogText.append(space); 191 | ogText.append(textObj); 192 | } 193 | } else { 194 | var textObj = new Text( 195 | urlScheme(urlStr), 196 | selectedItem.style 197 | ); 198 | selectedItem.setValueForColumn( 199 | textObj, 200 | selectedColumn 201 | ); 202 | } 203 | } else if (buttonIndex === 2) { 204 | console.log("adding large file"); 205 | var wrapper = FileWrapper.withContents( 206 | filename, 207 | data 208 | ); 209 | if (ogText) { 210 | var textObj = 211 | Text.makeFileAttachment( 212 | wrapper, 213 | ogText.style 214 | ); 215 | ogText.append(textObj); 216 | } else { 217 | var textObj = 218 | Text.makeFileAttachment( 219 | wrapper, 220 | selectedItem.style 221 | ); 222 | selectedItem.setValueForColumn( 223 | textObj, 224 | selectedColumn 225 | ); 226 | } 227 | } 228 | }); 229 | } else { 230 | var wrapper = FileWrapper.withContents( 231 | filename, 232 | data 233 | ); 234 | if (ogText) { 235 | var textObj = Text.makeFileAttachment( 236 | wrapper, 237 | ogText.style 238 | ); 239 | ogText.append(textObj); 240 | } else { 241 | var textObj = Text.makeFileAttachment( 242 | wrapper, 243 | selectedItem.style 244 | ); 245 | selectedItem.setValueForColumn( 246 | textObj, 247 | selectedColumn 248 | ); 249 | } 250 | } 251 | }); 252 | } 253 | }); 254 | }); 255 | 256 | // PROMISE FUNCTION CALLED UPON PICKER CANCELLATION 257 | pickerPromise.catch(function (error) { 258 | console.log("form cancelled", error.message); 259 | }); 260 | 261 | /* 262 | // Work around a bug that crops images by forcing UI to update 263 | var ogAlignment = selectedColumn.textAlignment 264 | if (ogAlignment === TextAlignment.Natural) { 265 | selectedColumn.textAlignment = TextAlignment.Left 266 | } else { 267 | selectedColumn.textAlignment = TextAlignment.Natural 268 | } 269 | selectedColumn.textAlignment = ogAlignment 270 | */ 271 | }); 272 | 273 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 274 | formPromise.catch(function (err) { 275 | console.log("form cancelled", err.message); 276 | }); 277 | }); 278 | 279 | action.validate = function (selection, sender) { 280 | // selection options: columns, document, editor, items, nodes, styles 281 | if (selection.items.length === 1) { 282 | return true; 283 | } else { 284 | return false; 285 | } 286 | }; 287 | 288 | return action; 289 | })(); 290 | 291 | function urlScheme(urlStr) { 292 | var result = urlStr; 293 | 294 | if (app.platformName === "iOS" || app.platformName === "iPadOS") { 295 | // Convert url to Files.app url 296 | var filesURL = urlStr.replace(/^file\:\/\/\//, "shareddocuments:///"); 297 | result = filesURL; 298 | } 299 | 300 | return result + " "; 301 | } 302 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/addLink.js: -------------------------------------------------------------------------------- 1 | // This action inserts a URL hyperlink to the end of the selected column of selected rows. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | var selectedItems = selection.items; 7 | 8 | // List all visible text columns for insertion 9 | editor = document.editors[0]; 10 | filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | var defaultText = ""; 32 | var defaultURL = ""; 33 | if (Pasteboard.general.hasURLs) { 34 | var defaultURL = Pasteboard.general.URL.string; 35 | } else if (Pasteboard.general.hasStrings) { 36 | var defaultText = Pasteboard.general.string; 37 | } 38 | 39 | // CREATE FORM FOR GATHERING USER INPUT 40 | var inputForm = new Form(); 41 | 42 | // CREATE TEXT FIELD 43 | var textField = new Form.Field.String( 44 | "textInput", 45 | "Title", 46 | defaultText, 47 | null 48 | ); 49 | 50 | var urlField = new Form.Field.String( 51 | "urlInput", 52 | "URL", 53 | defaultURL, 54 | null 55 | ); 56 | 57 | if (filteredColumns.includes(document.outline.outlineColumn)) { 58 | var defaultColumn = document.outline.outlineColumn; 59 | } else { 60 | var defaultColumn = document.outline.noteColumn; 61 | } 62 | 63 | var columnField = new Form.Field.Option( 64 | "columnInput", 65 | "Column", 66 | filteredColumns, 67 | filteredColumnTitles, 68 | defaultColumn 69 | ); 70 | 71 | var insertionPositionField = new Form.Field.Option( 72 | "insertionPositionInput", 73 | "Position", 74 | ["End", "Start"], 75 | null, 76 | "End" 77 | ); 78 | // ADD THE FIELDS TO THE FORM 79 | inputForm.addField(textField); 80 | inputForm.addField(urlField); 81 | inputForm.addField(columnField); 82 | inputForm.addField(insertionPositionField); 83 | 84 | // PRESENT THE FORM TO THE USER 85 | formPrompt = "Enter URL and select Column"; 86 | formPromise = inputForm.show(formPrompt, "Continue"); 87 | 88 | // VALIDATE THE USER INPUT 89 | inputForm.validate = function (formObject) { 90 | var urlValue = formObject.values["urlInput"]; 91 | var urlStatus = 92 | urlValue && urlValue.length > 0 && urlValue.match(/:\/\//) 93 | ? true 94 | : false; 95 | return urlStatus; 96 | }; 97 | 98 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 99 | formPromise.then(function (formObject) { 100 | var insertStr = formObject.values["textInput"].trim(); 101 | var urlStr = formObject.values["urlInput"].trim(); 102 | var selectedColumn = formObject.values["columnInput"]; 103 | var selectedPosition = formObject.values["insertionPositionInput"]; 104 | var url = URL.fromString(urlStr); 105 | 106 | selectedItems.forEach(function (item) { 107 | var targetText = item.valueForColumn(selectedColumn); 108 | if (targetText) { 109 | if (insertStr === "") { 110 | insertStr = urlStr; 111 | var textInsert = new Text(insertStr, targetText.style); 112 | } else { 113 | var textInsert = new Text(insertStr, targetText.style); 114 | textInsert.style.set(Style.Attribute.Link, url); 115 | } 116 | var space = new Text(" ", targetText.style); 117 | if (selectedPosition === "End") { 118 | if ( 119 | targetText.string && 120 | !targetText.string.slice(-1).match(/\s/) 121 | ) { 122 | targetText.append(space); 123 | } 124 | targetText.append(textInsert); 125 | targetText.append(space); 126 | } else if (selectedPosition === "Start") { 127 | if ( 128 | targetText.string && 129 | !targetText.string.slice(0, 1).match(/\s/) 130 | ) { 131 | targetText.insert(targetText.start, space); 132 | } 133 | targetText.insert(targetText.start, textInsert); 134 | } 135 | } else { 136 | if (insertStr === "") { 137 | insertStr = urlStr; 138 | var textInsert = new Text(insertStr, item.style); 139 | } else { 140 | var textInsert = new Text(insertStr, item.style); 141 | textInsert.style.set(Style.Attribute.Link, url); 142 | } 143 | var space = new Text(" ", item.style); 144 | textInsert.append(space); 145 | item.setValueForColumn(textInsert, selectedColumn); 146 | } 147 | }); 148 | 149 | // Work around a bug that crops images by forcing UI to update 150 | editor.setVisibilityForColumn(selectedColumn, false); 151 | editor.setVisibilityForColumn(selectedColumn, true); 152 | }); 153 | 154 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 155 | formPromise.catch(function (err) { 156 | console.log("form cancelled", err.message); 157 | }); 158 | }); 159 | 160 | action.validate = function (selection, sender) { 161 | // selection options: columns, document, editor, items, nodes, styles 162 | if (selection.items.length > 0) { 163 | return true; 164 | } else { 165 | return false; 166 | } 167 | }; 168 | 169 | return action; 170 | })(); 171 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/addText.js: -------------------------------------------------------------------------------- 1 | // This action inserts texts at the end or start of the selected column of selected rows. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | var selectedItems = selection.items; 7 | 8 | // List all visible text columns for insertion 9 | editor = document.editors[0]; 10 | filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | var defaultText = ""; 32 | if (Pasteboard.general.hasStrings) { 33 | var defaultText = Pasteboard.general.string; 34 | } 35 | 36 | // CREATE FORM FOR GATHERING USER INPUT 37 | var inputForm = new Form(); 38 | 39 | // CREATE TEXT FIELD 40 | var textField = new Form.Field.String("textInput", "Text", defaultText); 41 | 42 | if (filteredColumns.includes(document.outline.outlineColumn)) { 43 | var defaultColumn = document.outline.outlineColumn; 44 | } else { 45 | var defaultColumn = document.outline.noteColumn; 46 | } 47 | 48 | var colourField = new Form.Field.Checkbox( 49 | "colourInput", 50 | "Colour", 51 | false 52 | ); 53 | 54 | var defaultRGB = "(1, 0, 0, 1)"; 55 | var rgbField = function (str) { 56 | return new Form.Field.String("rgbInput", "RGB", str); 57 | }; 58 | var columnField = new Form.Field.Option( 59 | "columnInput", 60 | "Column", 61 | filteredColumns, 62 | filteredColumnTitles, 63 | defaultColumn 64 | ); 65 | 66 | var insertionPositionField = new Form.Field.Option( 67 | "insertionPositionInput", 68 | "Position", 69 | ["End", "Start"], 70 | null, 71 | "End" 72 | ); 73 | 74 | // ADD THE FIELDS TO THE FORM 75 | inputForm.addField(textField); 76 | inputForm.addField(columnField); 77 | inputForm.addField(insertionPositionField); 78 | inputForm.addField(colourField); 79 | // PRESENT THE FORM TO THE USER 80 | formPrompt = "Enter Text and select Column"; 81 | formPromise = inputForm.show(formPrompt, "Continue"); 82 | 83 | // VALIDATE THE USER INPUT 84 | inputForm.validate = function (formObject) { 85 | var keys = formObject.fields.map((field) => field.key); 86 | 87 | var textValue = formObject.values["textInput"]; 88 | var textStatus = textValue && textValue.length > 0 ? true : false; 89 | 90 | if (keys.indexOf("rgbInput") === -1) { 91 | if (formObject.values["colourInput"]) { 92 | formObject.addField(rgbField(defaultRGB)); 93 | } 94 | } else { 95 | if (formObject.values["colourInput"]) { 96 | var rgb = formObject.values["rgbInput"]; 97 | if ( 98 | rgb.match( 99 | /\(\s*\d+\.{0,1}\d*\s*,\s*\d+\.{0,1}\d*\s*,\s*\d+\.{0,1}\d*\s*,\s*\d+\.{0,1}\d*\s*\)/ 100 | ) === null 101 | ) { 102 | return false; 103 | } else { 104 | var arr = rgb.match(/\d+\.{0,1}\d*/g); 105 | for (var i = 0; i < arr.length; i++) { 106 | var float = parseFloat(arr[i]); 107 | if (float > 1) { 108 | return false; 109 | } 110 | } 111 | } 112 | } else { 113 | defaultRGB = formObject.values["rgbInput"]; 114 | formObject.removeField( 115 | formObject.fields[keys.indexOf("rgbInput")] 116 | ); 117 | } 118 | } 119 | return textStatus; 120 | }; 121 | 122 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 123 | formPromise.then(function (formObject) { 124 | var insertStr = formObject.values["textInput"]; 125 | var selectedColumn = formObject.values["columnInput"]; 126 | var selectedPosition = formObject.values["insertionPositionInput"]; 127 | 128 | var fillColour = formObject.values["colourInput"]; 129 | if (fillColour) { 130 | var rgb = formObject.values["rgbInput"]; 131 | var floatArr = rgb 132 | .match(/\d+\.{0,1}\d*/g) 133 | .map((str) => parseFloat(str)); 134 | var colour = Color.RGB( 135 | floatArr[0], 136 | floatArr[1], 137 | floatArr[2], 138 | floatArr[3] 139 | ); 140 | } 141 | 142 | selectedItems.forEach(function (item) { 143 | var targetText = item.valueForColumn(selectedColumn); 144 | if (targetText) { 145 | var textInsert = new Text(insertStr, targetText.style); 146 | if (fillColour) { 147 | textInsert.style.set( 148 | Style.Attribute.FontFillColor, 149 | colour 150 | ); 151 | } 152 | if (selectedPosition === "End") { 153 | targetText.append(textInsert); 154 | } else if (selectedPosition === "Start") { 155 | targetText.insert(targetText.start, textInsert); 156 | } 157 | } else { 158 | var textInsert = new Text(insertStr, item.style); 159 | if (fillColour) { 160 | textInsert.style.set( 161 | Style.Attribute.FontFillColor, 162 | colour 163 | ); 164 | } 165 | item.setValueForColumn(textInsert, selectedColumn); 166 | } 167 | }); 168 | 169 | // Work around a bug that crops images by forcing UI to update 170 | editor.setVisibilityForColumn(selectedColumn, false); 171 | editor.setVisibilityForColumn(selectedColumn, true); 172 | }); 173 | 174 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 175 | formPromise.catch(function (err) { 176 | console.log("form cancelled", err.message); 177 | }); 178 | }); 179 | 180 | action.validate = function (selection, sender) { 181 | // selection options: columns, document, editor, items, nodes, styles 182 | if (selection.items.length > 0) { 183 | return true; 184 | } else { 185 | return false; 186 | } 187 | }; 188 | 189 | return action; 190 | })(); 191 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/computeColumn.js: -------------------------------------------------------------------------------- 1 | // This action computes the entered formula with eval() from values of selected columns, and outputs the results into the selected target column. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | var items = selection.items; 8 | var itemsLength = items.length; 9 | 10 | // List all visible text columns for insertion 11 | editor = document.editors[0]; 12 | filteredColumns = columns.filter(function (column) { 13 | if (editor.visibilityOfColumn(column)) { 14 | if ( 15 | column.type === Column.Type.Number || 16 | column.type === Column.Type.Duration || 17 | column.type === Column.Type.Date || 18 | column.type === Column.Type.Checkbox 19 | ) { 20 | return column; 21 | } 22 | } 23 | }); 24 | 25 | if (filteredColumns.length === 0) { 26 | throw new Error( 27 | "This document has no visible columns of valid types." 28 | ); 29 | } 30 | 31 | // CREATE FORM FOR GATHERING USER INPUT 32 | var inputForm = new Form(); 33 | 34 | // CREATE TEXT FIELD 35 | 36 | if (filteredColumns.indexOf(document.outline.outlineColumn) !== -1) { 37 | var defaultTarget = document.outline.outlineColumn; 38 | } else { 39 | var defaultTarget = filteredColumns[0]; 40 | } 41 | 42 | // Output column field 43 | var targetField = function (columns, col) { 44 | return new Form.Field.Option( 45 | "targetInput", 46 | "Output", 47 | columns, 48 | columns.map(function (column) { 49 | return column.title; 50 | }), 51 | col 52 | ); 53 | }; 54 | 55 | // Override or append to existing contents 56 | var overrideToggle = new Form.Field.Checkbox( 57 | "overrideInput", 58 | "Override", 59 | true 60 | ); 61 | 62 | // Formula field function to update during validation 63 | var formulaField = function (str) { 64 | return new Form.Field.String("formulaInput", "Formula", str); 65 | }; 66 | 67 | // Define a dynamic field to add and remove during validation 68 | var columnField = function (columns, index, col) { 69 | return new Form.Field.Option( 70 | "columnInput" + index, 71 | "Column " + index, 72 | columns, 73 | columns.map(function (column) { 74 | return column.title; 75 | }), 76 | col 77 | ); 78 | }; 79 | 80 | // Determine the column options for the input comlumn fields 81 | var inputColumnOptions = function (targetColumn, selectedInputColumns) { 82 | var columns = filteredColumns.filter(function (column) { 83 | if (!selectedInputColumns.includes(column)) { 84 | return column; 85 | } 86 | }); 87 | 88 | var columns = columns.filter(function (column) { 89 | if ( 90 | targetColumn.type === Column.Type.Date || 91 | targetColumn.type === Column.Type.Duration 92 | ) { 93 | // Allow only date and duration inputs 94 | if ( 95 | column.type === Column.Type.Duration || 96 | column.type === Column.Type.Date 97 | ) { 98 | return column; 99 | } 100 | } else if (targetColumn.type === Column.Type.Number) { 101 | // Allow only number inputs 102 | if (column.type === Column.Type.Number) { 103 | return column; 104 | } 105 | } else if (targetColumn.type === Column.Type.Checkbox) { 106 | // Allow only number inputs 107 | if (column.type === Column.Type.Checkbox) { 108 | return column; 109 | } 110 | } 111 | }); 112 | 113 | return columns; 114 | }; 115 | 116 | // ADD THE FIELDS TO THE FORM 117 | inputForm.addField(targetField(filteredColumns, null)); 118 | var targetIsFixed = false; 119 | 120 | inputForm.addField(overrideToggle); 121 | inputForm.addField(formulaField("")); 122 | 123 | var selectedInputColumns = []; // No columns are selected now. 124 | 125 | var inputLength = 0; // Keep track of the number of input column fields 126 | 127 | // PRESENT THE FORM TO THE USER 128 | formPrompt = "Compute Formula to Column"; 129 | formPromise = inputForm.show(formPrompt, "Continue"); 130 | 131 | // VALIDATE THE USER INPUT 132 | inputForm.validate = function (formObject) { 133 | var keys = formObject.fields.map((field) => field.key); 134 | 135 | var selectedTargetColumn = formObject.values["targetInput"]; 136 | 137 | var formulaFieldIndex = keys.indexOf("formulaInput"); 138 | var formulaInput = formObject.values["formulaInput"]; 139 | 140 | // Add first input column field and fix target 141 | if (selectedTargetColumn && !targetIsFixed) { 142 | formObject.addField( 143 | columnField( 144 | inputColumnOptions( 145 | selectedTargetColumn, 146 | selectedInputColumns 147 | ), 148 | 0, 149 | null 150 | ) 151 | ); 152 | 153 | inputLength = inputLength + 1; 154 | 155 | var index = keys.indexOf("targetInput"); 156 | formObject.removeField(formObject.fields[index]); 157 | formObject.addField( 158 | targetField([selectedTargetColumn], selectedTargetColumn), 159 | index 160 | ); 161 | targetIsFixed = true; 162 | } 163 | 164 | if ( 165 | keys.indexOf("columnInput" + (inputLength - 1)) && 166 | formObject.values["columnInput" + (inputLength - 1)] 167 | ) { 168 | var column = 169 | formObject.values["columnInput" + (inputLength - 1)]; 170 | var index = keys.indexOf("columnInput" + (inputLength - 1)); 171 | 172 | // Fix last column option 173 | if (selectedInputColumns.length === inputLength - 1) { 174 | formObject.removeField(formObject.fields[index]); 175 | formObject.addField( 176 | columnField([column], inputLength - 1, column), 177 | index 178 | ); 179 | 180 | // Add this variable name to formula field 181 | let regex = new RegExp("C" + (inputLength - 1), "gi"); 182 | if (!formulaInput.match(regex)) { 183 | if (formulaInput) { 184 | formulaInput = 185 | formulaInput + " C" + (inputLength - 1); 186 | } else { 187 | formulaInput = 188 | formulaInput + "C" + (inputLength - 1); 189 | } 190 | formObject.removeField( 191 | formObject.fields[formulaFieldIndex] 192 | ); 193 | formObject.addField( 194 | formulaField(formulaInput), 195 | formulaFieldIndex 196 | ); 197 | } 198 | } 199 | if (!selectedInputColumns.includes(column)) { 200 | selectedInputColumns.push(column); 201 | } 202 | 203 | if ( 204 | inputColumnOptions( 205 | selectedTargetColumn, 206 | selectedInputColumns 207 | ).length !== 0 && 208 | keys.indexOf("columnInput" + inputLength) === -1 209 | ) { 210 | // Add next column option 211 | formObject.addField( 212 | columnField( 213 | inputColumnOptions( 214 | selectedTargetColumn, 215 | selectedInputColumns 216 | ), 217 | inputLength, 218 | null 219 | ) 220 | ); 221 | 222 | inputLength = inputLength + 1; 223 | } 224 | 225 | console.log( 226 | selectedInputColumns.length + 227 | " out of " + 228 | inputLength + 229 | " input column fields are selected on the form." 230 | ); 231 | } 232 | 233 | // Two dates are only allowed to be substracted 234 | if ( 235 | selectedInputColumns.length === 2 && 236 | selectedInputColumns[0].type === Column.Type.Date && 237 | selectedInputColumns[1].type === Column.Type.Date 238 | ) { 239 | if (formulaInput.match(/(\bC1\b|\bC0\b)/gi).length % 2 !== 0) { 240 | throw new Error("Dates C0 and C1 must appear in pairs."); 241 | } 242 | } 243 | 244 | // Valid date formula with fake data 245 | if (formObject.values["targetInput"]) { 246 | if ( 247 | isValidFormula( 248 | formulaInput, 249 | selectedInputColumns, 250 | formObject.values["targetInput"] 251 | ) 252 | ) { 253 | return true; 254 | } else { 255 | throw new Error("Formula is invalid."); 256 | } 257 | } else { 258 | throw new Error("Select an output column."); 259 | } 260 | }; 261 | 262 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 263 | formPromise.then(function (formObject) { 264 | var target = formObject.values["targetInput"]; 265 | var override = formObject.values["overrideInput"]; 266 | var formula = formObject.values["formulaInput"]; 267 | var columns = selectedInputColumns; 268 | 269 | items.forEach((item, index) => { 270 | console.log("computing for row ", index); 271 | var ogValue = item.valueForColumn(target); 272 | var result = compute(formula, item, columns, target); 273 | 274 | if (override) { 275 | item.setValueForColumn(result, target); 276 | } else if (!ogValue) { 277 | item.setValueForColumn(result, target); 278 | } 279 | }); 280 | }); 281 | 282 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 283 | formPromise.catch(function (err) { 284 | console.log("form cancelled", err.message); 285 | }); 286 | }); 287 | action.validate = function (selection, sender) { 288 | // validation code 289 | // selection options: columns, document, editor, items, nodes, styles 290 | editor = document.editors[0]; 291 | filteredColumns = columns.filter(function (column) { 292 | if (editor.visibilityOfColumn(column)) { 293 | if ( 294 | column.type === Column.Type.Number || 295 | column.type === Column.Type.Duration || 296 | column.type === Column.Type.Date || 297 | column.type === Column.Type.Checkbox 298 | ) { 299 | return column; 300 | } 301 | } 302 | }); 303 | 304 | if (selection.items.length > 0 && filteredColumns.length !== 0) { 305 | return true; 306 | } else { 307 | return false; 308 | } 309 | }; 310 | 311 | return action; 312 | })(); 313 | _; 314 | 315 | // https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript 316 | function isValidDate(d) { 317 | return d instanceof Date && !isNaN(d); 318 | } 319 | 320 | // https://stackoverflow.com/questions/2140627/how-to-do-case-insensitive-string-comparison 321 | function ciEquals(a, b) { 322 | return typeof a === "string" && typeof b === "string" 323 | ? a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0 324 | : a === b; 325 | } 326 | 327 | function compute(formula, row, columns, target) { 328 | var result = 0; 329 | var c = columns.map((col) => { 330 | var value = row.valueForColumn(col); 331 | 332 | if (value) { 333 | if (value.constructor.name === "Decimal") { 334 | // Converts the custom Decimal objects to the native Number objects 335 | value = Number(value.toString()); 336 | } else if (value.constructor.name === "Sate") { 337 | // Converts the checkbox State objects to the Boolean objects 338 | if (value === Sate.Checked) { 339 | value = true; 340 | } else { 341 | value = false; 342 | } 343 | } else if (value.constructor.name === "Date") { 344 | // Use hours instead of milliseconds in conputing dates 345 | value = Number(value) / 3600000; 346 | } 347 | } else { 348 | // Null, undefined, or 0 values are redefined. 349 | if (col.type === Column.Type.Checkbox) { 350 | value = false; 351 | } else { 352 | value = 0; 353 | } 354 | } 355 | 356 | return value; 357 | }); 358 | 359 | formula = formula.replace(/\bC\d+\b/gi, (x) => { 360 | x = x.replace(/C/gi, "c["); 361 | return x + "]"; 362 | }); 363 | 364 | // Null result if formula doesn't pass 365 | try { 366 | result = eval(formula); 367 | } catch (err) { 368 | console.log(err); 369 | result = null; 370 | } 371 | 372 | if (target.type === Column.Type.Date) { 373 | if ((result && Number(result)) || result === 0) { 374 | result = new Date(Number(eval(formula) * 3600000)); 375 | } else { 376 | result = null; 377 | } 378 | } else if ( 379 | target.type === Column.Type.Duration || 380 | target.type === Column.Type.Number 381 | ) { 382 | if ((result && Number(result)) || result === 0) { 383 | result = Number(result); 384 | } else { 385 | result = null; 386 | } 387 | } else if (target.type === Column.Type.Checkbox) { 388 | if (result && Number(result)) { 389 | result = Boolean(result); 390 | } else { 391 | result = false; 392 | } 393 | 394 | if (result) { 395 | result = State.Checked; 396 | } else { 397 | result = State.Unchecked; 398 | } 399 | } else if (!result && result !== 0 && result !== false) { 400 | result = null; 401 | } 402 | 403 | // Final check on correct result types 404 | if (result) { 405 | if ( 406 | (target.type === Column.Type.Number || 407 | target.type === Column.Type.Duration) && 408 | typeof result !== "number" 409 | ) { 410 | result = null; 411 | } else if ( 412 | target.type === Column.Type.Checkbox && 413 | result.constructor.name !== "Sate" 414 | ) { 415 | result = State.Unchecked; 416 | } else if (target.type === Column.Type.Date && !isValidDate(result)) { 417 | result = null; 418 | } 419 | } 420 | 421 | console.log( 422 | "formula: ", 423 | formula, 424 | "\n", 425 | "variables: ", 426 | c, 427 | "\n", 428 | "result: ", 429 | result 430 | ); 431 | return result; 432 | } 433 | 434 | function isValidFormula(formula, columns, target) { 435 | var result = 0; 436 | var c = columns.map((col) => { 437 | if (col.type === Column.Type.Checkbox) { 438 | return false; 439 | } else { 440 | return 0; 441 | } 442 | }); 443 | 444 | formula = formula.replace(/\bC\d+\b/gi, (x) => { 445 | x = x.replace(/C/gi, "c["); 446 | return x + "]"; 447 | }); 448 | 449 | if (formula === "") { 450 | return true; 451 | } 452 | 453 | try { 454 | result = eval(formula); 455 | } catch (err) { 456 | console.log("Invalid Formula: Formula doesn't pass eval()."); 457 | return false; 458 | } 459 | 460 | if (result) { 461 | if ( 462 | typeof result === "number" || 463 | !isNaN(Number(result)) || 464 | typeof result === "boolean" || 465 | isValidDate(result) 466 | ) { 467 | console.log( 468 | "Valid Formula: Result is ", 469 | result, 470 | " of type", 471 | typeof result, 472 | "." 473 | ); 474 | return true; 475 | } else { 476 | console.log( 477 | "Invalid Formula: Result is ", 478 | result, 479 | " of type", 480 | typeof result, 481 | "." 482 | ); 483 | return false; 484 | } 485 | } else if (result === 0 || result === null) { 486 | console.log( 487 | "Valid Formula: Result is ", 488 | result, 489 | " of type", 490 | typeof result, 491 | "." 492 | ); 493 | return true; 494 | } else { 495 | console.log("Invalid Formula: Result is of type", typeof result, "."); 496 | return false; 497 | } 498 | } 499 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/copyAsLink.js: -------------------------------------------------------------------------------- 1 | // This action copies the item link(s) for the selected row(s). 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | if (selection.items.length > 1) { 8 | // CREATE FORM FOR GATHERING USER INPUT 9 | var inputForm = new Form(); 10 | 11 | var arrayToggle = new Form.Field.Checkbox( 12 | "arrayToggleInput", 13 | "Copy as Array", 14 | false 15 | ); 16 | 17 | inputForm.addField(arrayToggle); 18 | // PRESENT THE FORM TO THE USER 19 | formPrompt = "Copy as Link"; 20 | formPromise = inputForm.show(formPrompt, "Continue"); 21 | 22 | // VALIDATE THE USER INPUT 23 | inputForm.validate = function (formObject) { 24 | return null; 25 | }; 26 | 27 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 28 | formPromise.then(function (formObject) { 29 | var shareAsArray = formObject.values["arrayToggleInput"]; 30 | 31 | linksArray = []; 32 | selection.items.forEach(function (item) { 33 | itemLink = "omnioutliner:///open?row=" + item.identifier; 34 | linksArray.push(itemLink); 35 | }); 36 | 37 | if (shareAsArray) { 38 | Pasteboard.general.strings = linksArray; 39 | } else { 40 | Pasteboard.general.strings = [linksArray.join("\n")]; 41 | } 42 | }); 43 | 44 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 45 | formPromise.catch(function (err) { 46 | console.log("form cancelled", err.message); 47 | }); 48 | } else { 49 | itemLink = 50 | "omnioutliner:///open?row=" + 51 | selection.items[0].identifier + 52 | "\n"; 53 | Pasteboard.general.strings = [itemLink]; 54 | } 55 | }); 56 | 57 | action.validate = function (selection, sender) { 58 | // validation code 59 | // selection options: columns, document, editor, items, nodes, styles 60 | if (selection.items.length > 0) { 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | }; 66 | 67 | return action; 68 | })(); 69 | _; 70 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/copyColumn.js: -------------------------------------------------------------------------------- 1 | // This action copies the texts from selected column from selected rows. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | const Lib = this.plugIn.library("ApplicationLib"); 6 | // selection options: columns, document, editor, items, nodes, styles 7 | var columnTitles = columns.map(function (column) { 8 | if (column.title !== "") { 9 | return column.title; 10 | } else if (column === document.outline.noteColumn) { 11 | // The note column is the only text column with empty title 12 | return "Notes"; 13 | } else if (column === document.outline.statusColumn) { 14 | // The note column is the only text column with empty title 15 | return "Status"; 16 | } 17 | }); 18 | 19 | // CREATE FORM FOR GATHERING USER INPUT 20 | var inputForm = new Form(); 21 | 22 | // CREATE TEXT FIELD 23 | var columnField = new Form.Field.Option( 24 | "columnInput", 25 | "Column", 26 | columns, 27 | columnTitles, 28 | document.outline.outlineColumn 29 | ); 30 | inputForm.addField(columnField); 31 | 32 | if (selection.items.length > 1) { 33 | var arrayToggle = new Form.Field.Checkbox( 34 | "arrayToggleInput", 35 | "Copy as Array", 36 | false 37 | ); 38 | inputForm.addField(arrayToggle); 39 | } 40 | // PRESENT THE FORM TO THE USER 41 | formPrompt = "Copy Column"; 42 | formPromise = inputForm.show(formPrompt, "Continue"); 43 | 44 | // VALIDATE THE USER INPUT 45 | inputForm.validate = function (formObject) { 46 | return null; 47 | }; 48 | 49 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 50 | formPromise.then(function (formObject) { 51 | var col = formObject.values["columnInput"]; 52 | var shareAsArray = formObject.values["arrayToggleInput"]; 53 | var strings = selection.items.map((item) => { 54 | var value = item.valueForColumn(col); 55 | /* 56 | All column types: 57 | var Checkbox → Column.Type read-only 58 | var Date → Column.Type read-only 59 | var Duration → Column.Type read-only 60 | var Enumeration → Column.Type read-only 61 | var Number → Column.Type read-only 62 | var Text → Column.Type read-only 63 | */ 64 | 65 | if (col.type === Column.Type.Text) { 66 | // Value is a Text object 67 | if (value) { 68 | return Lib.textToMD(value); 69 | } else { 70 | return "\n"; 71 | } 72 | } else if (col.type === Column.Type.Checkbox) { 73 | // Value is a State object 74 | if (value === State.Checked) { 75 | return "checked"; 76 | } else if (value === State.Unchecked) { 77 | return "unchecked"; 78 | } else if (value === State.Mixed) { 79 | return "mixed"; 80 | } else { 81 | return "\n"; 82 | } 83 | } else if (col.type === Column.Type.Date) { 84 | // Value is a Date object 85 | if (value) { 86 | return value.toString(); 87 | } else { 88 | return "\n"; 89 | } 90 | } else if ( 91 | col.type === Column.Type.Duration || 92 | col.type === Column.Type.Number 93 | ) { 94 | // Value is a Decimal object 95 | if (value) { 96 | return value.toString(); 97 | } else { 98 | return "\n"; 99 | } 100 | } else if (col.type === Column.Type.Enumeration) { 101 | // Value is an Enumaration object 102 | if (value) { 103 | return value.name.trim(); 104 | } else { 105 | return "\n"; 106 | } 107 | } 108 | }); 109 | if (shareAsArray) { 110 | Pasteboard.general.strings = strings; 111 | } else { 112 | Pasteboard.general.strings = [strings.join("\n")]; 113 | } 114 | }); 115 | 116 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 117 | formPromise.catch(function (err) { 118 | console.log("form cancelled", err.message); 119 | }); 120 | }); 121 | 122 | action.validate = function (selection, sender) { 123 | // validation code 124 | // selection options: columns, document, editor, items, nodes, styles 125 | if (selection.items.length > 0) { 126 | return true; 127 | } else { 128 | return false; 129 | } 130 | }; 131 | 132 | return action; 133 | })(); 134 | _; 135 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/duplicateColumn.js: -------------------------------------------------------------------------------- 1 | // This action duplicates the chosen column and contents of all rows into the new column. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var selectedItems = selection.items; 7 | 8 | // List all visible columns 9 | var editor = document.editors[0]; 10 | var tree = document.outline; 11 | 12 | var filteredColumns = columns.filter(function (column) { 13 | if (editor.visibilityOfColumn(column)) { 14 | return column; 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no visible columns."); 20 | } 21 | 22 | var filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | // CREATE FORM FOR GATHERING USER INPUT 32 | var inputForm = new Form(); 33 | 34 | // CREATE TEXT FIELD 35 | 36 | if (filteredColumns.includes(document.outline.outlineColumn)) { 37 | var defaultColumn = document.outline.outlineColumn; 38 | } else { 39 | var defaultColumn = document.outline.noteColumn; 40 | } 41 | 42 | var columnField = new Form.Field.Option( 43 | "columnInput", 44 | "Column", 45 | filteredColumns, 46 | filteredColumnTitles, 47 | defaultColumn 48 | ); 49 | 50 | // ADD THE FIELDS TO THE FORM 51 | inputForm.addField(columnField); 52 | // PRESENT THE FORM TO THE USER 53 | formPrompt = "Select Column"; 54 | formPromise = inputForm.show(formPrompt, "Continue"); 55 | 56 | // VALIDATE THE USER INPUT 57 | inputForm.validate = function (formObject) { 58 | return null; 59 | }; 60 | 61 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 62 | formPromise.then(function (formObject) { 63 | var selectedColumn = formObject.values["columnInput"]; 64 | console.log("hi"); 65 | var newColumn = tree.addColumn( 66 | selectedColumn.type, 67 | editor.afterColumn(selectedColumn), 68 | function (column) { 69 | column.title = selectedColumn.title + " Copy"; 70 | try { 71 | if (selectedColumn.enumeration !== null) { 72 | column.enumeration = selectedColumn.enumeration; 73 | } 74 | } catch (err) { 75 | console.log(err.message); 76 | } 77 | try { 78 | if (selectedColumn.formatter !== null) { 79 | column.formatter = selectedColumn.formatter; 80 | } 81 | } catch (err) { 82 | console.log(err.message); 83 | } 84 | try { 85 | if (selectedColumn.style !== null) { 86 | column.setStyle(selectedColumn.style); 87 | } 88 | } catch (err) { 89 | console.log(err.message); 90 | } 91 | try { 92 | if (selectedColumn.textAlignment !== null) { 93 | column.textAlignment = selectedColumn.textAlignment; 94 | } 95 | } catch (err) { 96 | console.log(err.message); 97 | } 98 | } 99 | ); 100 | 101 | rootItem.descendants.forEach((item) => { 102 | item.setValueForColumn( 103 | item.valueForColumn(selectedColumn), 104 | newColumn 105 | ); 106 | }); 107 | }); 108 | 109 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 110 | formPromise.catch(function (err) { 111 | console.log("form cancelled", err.message); 112 | }); 113 | }); 114 | 115 | action.validate = function (selection, sender) { 116 | // validation code 117 | // selection options: columns, document, editor, items, nodes, styles 118 | if (document !== null) { 119 | return true; 120 | } else { 121 | return false; 122 | } 123 | }; 124 | 125 | return action; 126 | })(); 127 | _; 128 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/editColumn.js: -------------------------------------------------------------------------------- 1 | // This action edits the selected column of the selected rows in bulk, allowing for either override or fill. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | var items = selection.items; 8 | var itemsLength = items.length; 9 | 10 | // List all visible text columns for insertion 11 | editor = document.editors[0]; 12 | filteredColumns = columns.filter(function (column) { 13 | if (editor.visibilityOfColumn(column)) { 14 | return column; 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no visible columns."); 20 | } 21 | 22 | filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | var defaultText = ""; 32 | if (Pasteboard.general.hasStrings) { 33 | var defaultText = Pasteboard.general.string; 34 | } 35 | 36 | // CREATE FORM FOR GATHERING USER INPUT 37 | var inputForm = new Form(); 38 | 39 | // CREATE TEXT FIELD 40 | var textField = new Form.Field.String("textInput", "Text", defaultText); 41 | 42 | if (filteredColumns.indexOf(document.outline.outlineColumn) !== -1) { 43 | var defaultColumn = document.outline.outlineColumn; 44 | } else { 45 | var defaultColumn = document.outline.noteColumn; 46 | } 47 | 48 | var columnField = new Form.Field.Option( 49 | "columnInput", 50 | "Column", 51 | filteredColumns, 52 | filteredColumnTitles, 53 | defaultColumn 54 | ); 55 | 56 | // Override or append to existing contents 57 | var overrideToggle = new Form.Field.Checkbox( 58 | "overrideInput", 59 | "Override", 60 | true 61 | ); 62 | 63 | // ADD THE FIELDS TO THE FORM 64 | inputForm.addField(textField); 65 | inputForm.addField(columnField); 66 | inputForm.addField(overrideToggle); 67 | // PRESENT THE FORM TO THE USER 68 | formPrompt = "Paste Array to Column"; 69 | formPromise = inputForm.show(formPrompt, "Continue"); 70 | 71 | // VALIDATE THE USER INPUT 72 | inputForm.validate = function (formObject) { 73 | var textValue = formObject.values["textInput"]; 74 | var textStatus = textValue && textValue.length > 0 ? true : false; 75 | return textStatus 76 | }; 77 | 78 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 79 | formPromise.then(function (formObject) { 80 | var textStr = formObject.values["textInput"]; 81 | var column = formObject.values["columnInput"]; 82 | var override = formObject.values["overrideInput"]; 83 | 84 | var array = Array(itemsLength).fill(textStr); 85 | array.forEach((obj, index) => { 86 | var item = items[index]; 87 | 88 | var ogValue = item.valueForColumn(column); 89 | 90 | if (ogValue && column.type === Column.Type.Text) { 91 | var style = ogValue.style; 92 | } else { 93 | var style = item.style; 94 | } 95 | 96 | var newText = new Text(obj, style); 97 | 98 | // Modify items 99 | if (column.type === Column.Type.Text) { 100 | if (override) { 101 | item.setValueForColumn(newText, column); 102 | } else { 103 | if (ogValue) { 104 | if ( 105 | !ogValue.string.slice(-1).match(/\s/) && 106 | !newTextIsEmpty 107 | ) { 108 | var space = new Text(" ", style); 109 | ogValue.append(space); 110 | } 111 | ogValue.append(newText); 112 | } else { 113 | item.setValueForColumn(newText, column); 114 | } 115 | } 116 | } else if (column.type === Column.Type.Date) { 117 | var newDate = new Date(newText.string); 118 | if (isValidDate(newDate)) { 119 | if (override) { 120 | item.setValueForColumn(newDate, column); 121 | } else if (!ogValue) { 122 | item.setValueForColumn(newDate, column); 123 | } 124 | } else if (override) { 125 | item.setValueForColumn(null, column); 126 | } 127 | } else if ( 128 | column.type === Column.Type.Number || 129 | column.type === Column.Type.Duration 130 | ) { 131 | var newNumber = Number(newText.string); 132 | if (!isNaN(newNumber)) { 133 | if (override) { 134 | item.setValueForColumn(newNumber, column); 135 | } else if (!ogValue) { 136 | item.setValueForColumn(newNumber, column); 137 | } 138 | } else if (override) { 139 | item.setValueForColumn(null, column); 140 | } 141 | } else if (column.type === Column.Type.Enumeration) { 142 | var enumeration = column.enumeration; 143 | 144 | if ( 145 | !enumeration.memberNamed(newText.string) && 146 | newText.string && 147 | override 148 | ) { 149 | enumeration.add(newText.string); 150 | } 151 | item.setValueForColumn( 152 | enumeration.memberNamed(newText.string), 153 | column 154 | ); 155 | } else if (column.type === Column.Type.Checkbox) { 156 | if (override) { 157 | if (ciEquals(newText.string, "Checked")) { 158 | item.setValueForColumn(State.Checked, column); 159 | } else if (ciEquals(newText.string, "Unchecked")) { 160 | item.setValueForColumn(State.Unchecked, column); 161 | } 162 | } else { 163 | // If no overriding, only checking unchecked boxes are allowed. 164 | if (ciEquals(newText.string, "Checked")) { 165 | item.setValueForColumn(State.Checked, column); 166 | } 167 | } 168 | } 169 | }); 170 | }); 171 | 172 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 173 | formPromise.catch(function (err) { 174 | console.log("form cancelled", err.message); 175 | }); 176 | }); 177 | action.validate = function (selection, sender) { 178 | // validation code 179 | // selection options: columns, document, editor, items, nodes, styles 180 | if (selection.items.length > 0) { 181 | return true; 182 | } else { 183 | return false; 184 | } 185 | }; 186 | 187 | return action; 188 | })(); 189 | _; 190 | 191 | // https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript 192 | function isValidDate(d) { 193 | return d instanceof Date && !isNaN(d); 194 | } 195 | 196 | // https://stackoverflow.com/questions/2140627/how-to-do-case-insensitive-string-comparison 197 | function ciEquals(a, b) { 198 | return typeof a === "string" && typeof b === "string" 199 | ? a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0 200 | : a === b; 201 | } 202 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/addAttachment.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add Attachment"; 2 | "shortLabel" = "Attachment"; 3 | "mediumLabel" = "Add Attachment"; 4 | "longLabel" = "Add attachment to the selected row"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/addLink.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add Link"; 2 | "shortLabel" = "Add Link"; 3 | "mediumLabel" = "Add Link"; 4 | "LongLabel" = "Add link to selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/addText.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add Text"; 2 | "shortLabel" = "Add Text"; 3 | "mediumLabel" = "Add Text"; 4 | "longLabel" = "Add text to the selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/computeColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Compute Column"; 2 | "shortLabel" = "Compute Column"; 3 | "mediumLabel" = "Compute Column"; 4 | "LongLabel" = "Fill a selected column with computed values"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/copyAsLink.strings: -------------------------------------------------------------------------------- 1 | "label" = "Copy as Link"; 2 | "shortLabel" = "Copy Link"; 3 | "mediumLabel" = "Copy Link"; 4 | "LongLabel" = "Copy links from selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/copyColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Copy Column"; 2 | "shortLabel" = "Copy Column"; 3 | "mediumLabel" = "Copy Column"; 4 | "LongLabel" = "Copy column texts from selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/duplicateColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Duplicate Column"; 2 | "shortLabel" = "Duplicate Column"; 3 | "mediumLabel" = "Duplicate Column"; 4 | "longLabel" = "Duplicate column to the right"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/editColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Edit Column"; 2 | "shortLabel" = "Edit Column"; 3 | "mediumLabel" = "Edit Column"; 4 | "LongLabel" = "Edit selected colomn of selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/findAndReplace.strings: -------------------------------------------------------------------------------- 1 | "label" = "Find and Replace"; 2 | "shortLabel" = "Find"; 3 | "mediumLabel" = "Find and Replace"; 4 | "longLabel" = "Find and replace texts with RegEx"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.taxyovio.edit" = "⓵ Edit"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/pasteColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Paste Column"; 2 | "shortLabel" = "Paste Column"; 3 | "mediumLabel" = "Paste Column"; 4 | "LongLabel" = "Paste array into selected colomn of selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/en.lproj/renameAttachment.strings: -------------------------------------------------------------------------------- 1 | "label" = "Rename Attachment"; 2 | "shortLabel" = "Rename Attachment"; 3 | "mediumLabel" = "Rename Attachment"; 4 | "LongLabel" = "Rename attachments in selected rows"; -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taxyovio/OmniOutliner-Plug-Ins/bca08f8ae5409fcac616217c604b5cfcc5458dd9/edit.omnioutlinerjs/Resources/icon.png -------------------------------------------------------------------------------- /edit.omnioutlinerjs/Resources/pasteColumn.js: -------------------------------------------------------------------------------- 1 | // This action pastes the array of objects from Clipboard into the selected column of the selected rows. 2 | // The clipboard item list and row list are truncated to the length of the shorter one. 3 | var _ = (function () { 4 | var action = new PlugIn.Action(function (selection, sender) { 5 | // action code 6 | // selection options: columns, document, editor, items, nodes, styles 7 | var array = Pasteboard.general.items; 8 | var arrayLength = array.length; 9 | 10 | var items = selection.items; 11 | var itemsLength = items.length; 12 | 13 | // The number of items get pasted is limited by both array length and selection length 14 | if (arrayLength > itemsLength) { 15 | var length = itemsLength; 16 | array = array.slice(0, length); 17 | } else { 18 | var length = arrayLength; 19 | items = items.slice(0, length); 20 | } 21 | 22 | // List all visible text columns for insertion 23 | editor = document.editors[0]; 24 | filteredColumns = columns.filter(function (column) { 25 | if (editor.visibilityOfColumn(column)) { 26 | return column; 27 | } 28 | }); 29 | 30 | if (filteredColumns.length === 0) { 31 | throw new Error("This document has no visible columns."); 32 | } 33 | 34 | filteredColumnTitles = filteredColumns.map(function (column) { 35 | if (column.title !== "") { 36 | return column.title; 37 | } else if (column === document.outline.noteColumn) { 38 | // The note column has empty title for unknown reason 39 | return "Notes"; 40 | } 41 | }); 42 | 43 | // CREATE FORM FOR GATHERING USER INPUT 44 | var inputForm = new Form(); 45 | 46 | // CREATE TEXT FIELD 47 | 48 | if (filteredColumns.indexOf(document.outline.outlineColumn) !== -1) { 49 | var defaultColumn = document.outline.outlineColumn; 50 | } else { 51 | var defaultColumn = document.outline.noteColumn; 52 | } 53 | 54 | var columnField = new Form.Field.Option( 55 | "columnInput", 56 | "Column", 57 | filteredColumns, 58 | filteredColumnTitles, 59 | defaultColumn 60 | ); 61 | 62 | // Override or append to existing contents 63 | var overrideToggle = new Form.Field.Checkbox( 64 | "overrideInput", 65 | "Override", 66 | true 67 | ); 68 | 69 | // ADD THE FIELDS TO THE FORM 70 | inputForm.addField(columnField); 71 | inputForm.addField(overrideToggle); 72 | // PRESENT THE FORM TO THE USER 73 | formPrompt = "Paste Array to Column"; 74 | formPromise = inputForm.show(formPrompt, "Continue"); 75 | 76 | // VALIDATE THE USER INPUT 77 | inputForm.validate = function (formObject) { 78 | return null; 79 | }; 80 | 81 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 82 | formPromise.then(function (formObject) { 83 | var column = formObject.values["columnInput"]; 84 | var override = formObject.values["overrideInput"]; 85 | 86 | array.forEach((obj, index) => { 87 | var item = items[index]; 88 | 89 | var ogValue = item.valueForColumn(column); 90 | 91 | if (ogValue && column.type === Column.Type.Text) { 92 | var style = ogValue.style; 93 | } else { 94 | var style = item.style; 95 | } 96 | 97 | var newText = new Text("", style); 98 | var newTextIsEmpty = true; 99 | // Filter out proprietary Pasteboard.Item types 100 | var types = obj.types.filter((type) => { 101 | return ( 102 | type.identifier !== 103 | "com.omnigroup.omnioutliner.pboard.xmloutline.items" && 104 | type.identifier !== 105 | "com.omnigroup.omnioutliner.pboard.items-v3" && 106 | type.identifier !== "com.omnigroup.omnistyle.pboard.xml" 107 | ); 108 | }); 109 | 110 | // Construct new text obj 111 | if (types.length !== 0) { 112 | console.log( 113 | "clipboard object ", 114 | index + 1, 115 | "/", 116 | length, 117 | "is of type", 118 | types 119 | ); 120 | 121 | var textTypes = types.filter((type) => { 122 | return ( 123 | type.conformsTo(TypeIdentifier.plainText) || 124 | type.identifier === "public.utf8-plain-text" || 125 | type.identifier === "public.rtf" || 126 | type === TypeIdentifier.URL 127 | ); 128 | }); 129 | 130 | if (textTypes.length !== 0) { 131 | var str = ""; 132 | if ( 133 | textTypes.some((type) => 134 | type.conformsTo(TypeIdentifier.plainText) 135 | ) 136 | ) { 137 | str = obj.stringForType( 138 | types.find((type) => 139 | type.conformsTo(TypeIdentifier.plainText) 140 | ) 141 | ); 142 | } else if ( 143 | obj.types.some( 144 | (type) => 145 | type.identifier === "public.utf8-plain-text" 146 | ) 147 | ) { 148 | str = obj.stringForType( 149 | types.find( 150 | (type) => 151 | type.identifier === 152 | "public.utf8-plain-text" 153 | ) 154 | ); 155 | } else if ( 156 | obj.types.some( 157 | (type) => type === TypeIdentifier.URL 158 | ) 159 | ) { 160 | str = obj.stringForType( 161 | types.find( 162 | (type) => type === TypeIdentifier.URL 163 | ) 164 | ); 165 | } 166 | str = str.trim(); 167 | if (str) { 168 | var text = new Text(str, style); 169 | newText.append(text); 170 | newTextIsEmpty = false; 171 | } 172 | } 173 | 174 | var nonTextTypes = types.filter((type) => { 175 | return ( 176 | !type.conformsTo(TypeIdentifier.plainText) && 177 | type.identifier !== "public.utf8-plain-text" && 178 | type.identifier !== "public.rtf" && 179 | type.identifier !== "public.url" 180 | ); 181 | }); 182 | 183 | if (nonTextTypes.length !== 0) { 184 | nonTextTypes.forEach((type, i) => { 185 | var fileExtension = type.pathExtensions[0]; 186 | if (fileExtension) { 187 | var fileName = 188 | fileExtension.toUpperCase() + 189 | " " + 190 | index.toString() + 191 | i.toString() + 192 | "." + 193 | fileExtension; 194 | } else { 195 | var fileName = index.toString(); 196 | } 197 | var newWrapper = FileWrapper.withContents( 198 | fileName, 199 | obj.dataForType(type) 200 | ); 201 | var newFile = Text.makeFileAttachment( 202 | newWrapper, 203 | style 204 | ); 205 | newText.append(newFile); 206 | newTextIsEmpty = false; 207 | }); 208 | } 209 | } else { 210 | console.log( 211 | "clipboard object ", 212 | index + 1, 213 | "/", 214 | length, 215 | "has no valid type" 216 | ); 217 | } 218 | 219 | // Modify items 220 | if (column.type === Column.Type.Text) { 221 | if (override) { 222 | item.setValueForColumn(newText, column); 223 | } else { 224 | if (ogValue) { 225 | if ( 226 | !ogValue.string.slice(-1).match(/\s/) && 227 | !newTextIsEmpty 228 | ) { 229 | var space = new Text(" ", style); 230 | ogValue.append(space); 231 | } 232 | ogValue.append(newText); 233 | } else { 234 | item.setValueForColumn(newText, column); 235 | } 236 | } 237 | } else if (column.type === Column.Type.Date) { 238 | var newDate = new Date(newText.string); 239 | if (isValidDate(newDate)) { 240 | if (override) { 241 | item.setValueForColumn(newDate, column); 242 | } else if (!ogValue) { 243 | item.setValueForColumn(newDate, column); 244 | } 245 | } else if (override) { 246 | item.setValueForColumn(null, column); 247 | } 248 | } else if ( 249 | column.type === Column.Type.Number || 250 | column.type === Column.Type.Duration 251 | ) { 252 | var newNumber = Number(newText.string); 253 | if (!isNaN(newNumber)) { 254 | if (override) { 255 | item.setValueForColumn(newNumber, column); 256 | } else if (!ogValue) { 257 | item.setValueForColumn(newNumber, column); 258 | } 259 | } else if (override) { 260 | item.setValueForColumn(null, column); 261 | } 262 | } else if (column.type === Column.Type.Enumeration) { 263 | var enumeration = column.enumeration; 264 | 265 | if ( 266 | !enumeration.memberNamed(newText.string) && 267 | newText.string && 268 | override 269 | ) { 270 | enumeration.add(newText.string); 271 | } 272 | item.setValueForColumn( 273 | enumeration.memberNamed(newText.string), 274 | column 275 | ); 276 | } else if (column.type === Column.Type.Checkbox) { 277 | if (override) { 278 | if (ciEquals(newText.string, "Checked")) { 279 | item.setValueForColumn(State.Checked, column); 280 | } else if (ciEquals(newText.string, "Unchecked")) { 281 | item.setValueForColumn(State.Unchecked, column); 282 | } 283 | } else { 284 | // If no overriding, only checking unchecked boxes are allowed. 285 | if (ciEquals(newText.string, "Checked")) { 286 | item.setValueForColumn(State.Checked, column); 287 | } 288 | } 289 | } 290 | }); 291 | }); 292 | 293 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 294 | formPromise.catch(function (err) { 295 | console.log("form cancelled", err.message); 296 | }); 297 | }); 298 | action.validate = function (selection, sender) { 299 | // validation code 300 | // selection options: columns, document, editor, items, nodes, styles 301 | if (selection.items.length > 0) { 302 | return true; 303 | } else { 304 | return false; 305 | } 306 | }; 307 | 308 | return action; 309 | })(); 310 | _; 311 | 312 | // https://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript 313 | function isValidDate(d) { 314 | return d instanceof Date && !isNaN(d); 315 | } 316 | 317 | // https://stackoverflow.com/questions/2140627/how-to-do-case-insensitive-string-comparison 318 | function ciEquals(a, b) { 319 | return typeof a === "string" && typeof b === "string" 320 | ? a.localeCompare(b, undefined, { sensitivity: "accent" }) === 0 321 | : a === b; 322 | } 323 | -------------------------------------------------------------------------------- /edit.omnioutlinerjs/manifest.json: -------------------------------------------------------------------------------- 1 | {"author":"Taxyovio","libraries":[{"identifier":"ApplicationLib"}],"identifier":"com.taxyovio.edit","version":"2021.03.17","description":"A collection of Omni Automation scripts editing data in OmniOutliner.","actions":[ 2 | {"image": "doc.on.doc","identifier":"copyColumn"}, 3 | {"image":"doc.on.clipboard","identifier":"pasteColumn"}, 4 | {"image": "list.triangle","identifier":"editColumn"}, 5 | {"image":"function","identifier":"computeColumn"}, 6 | {"image":"plus.square.on.square","identifier":"duplicateColumn"}, 7 | {"image":"link","identifier":"copyAsLink"}, 8 | {"image":"text.magnifyingglass","identifier":"findAndReplace"}, 9 | {"image":"text.badge.plus","identifier":"addText"}, 10 | {"image":"link.badge.plus","identifier":"addLink"}, 11 | {"image":"paperclip","identifier":"addAttachment"}, 12 | {"image":"character.cursor.ibeam","identifier":"renameAttachment"}, 13 | ],"defaultLocale":"en"} 14 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/ApplicationLib.js: -------------------------------------------------------------------------------- 1 | var _ = (function () { 2 | var ApplicationLib = new PlugIn.Library(new Version("1.0")); 3 | 4 | ApplicationLib.isBeforeCurVers = function (versStrToCheck) { 5 | curVersStr = app.version; 6 | curVers = new Version(curVersStr); 7 | versToCheck = new Version(versStrToCheck); 8 | result = versToCheck.isBefore(curVers); 9 | console.log( 10 | versStrToCheck + " is before " + curVersStr + " = " + result 11 | ); 12 | return result; 13 | }; 14 | 15 | ApplicationLib.isEqualToCurVers = function (versStrToCheck) { 16 | curVersStr = app.version; 17 | curVers = new Version(curVersStr); 18 | versToCheck = new Version(versStrToCheck); 19 | result = versToCheck.equals(curVers); 20 | console.log(versStrToCheck + " equals " + curVersStr + " = " + result); 21 | return result; 22 | }; 23 | 24 | ApplicationLib.isAtLeastCurVers = function (versStrToCheck) { 25 | curVersStr = app.version; 26 | curVers = new Version(curVersStr); 27 | versToCheck = new Version(versStrToCheck); 28 | result = versToCheck.atLeast(curVers); 29 | console.log( 30 | versStrToCheck + " is at least " + curVersStr + " = " + result 31 | ); 32 | return result; 33 | }; 34 | 35 | ApplicationLib.isAfterCurVers = function (versStrToCheck) { 36 | curVersStr = app.version; 37 | curVers = new Version(curVersStr); 38 | versToCheck = new Version(versStrToCheck); 39 | result = versToCheck.isAfter(curVers); 40 | console.log( 41 | versStrToCheck + " is after " + curVersStr + " = " + result 42 | ); 43 | return result; 44 | }; 45 | 46 | // returns a list of functions 47 | ApplicationLib.handlers = function () { 48 | return "\n// ApplicationLib ©2021 Taxyovio\n• isBeforeCurVers(versStrToCheck)\n• isEqualToCurVers(versStrToCheck)\n• isAtLeastCurVers(versStrToCheck)\n• isAfterCurVers(versStrToCheck)"; 49 | }; 50 | 51 | // returns contents of matching strings file 52 | ApplicationLib.documentation = function () { 53 | // create a version object 54 | var aVersion = new Version("1.0"); 55 | // look up the plugin 56 | var plugin = PlugIn.find("com.Taxyovio.OmniOutliner", aVersion); 57 | // get the url for the text file inside this plugin 58 | var url = plugin.resourceNamed("ApplicationLib.strings"); 59 | // read the file 60 | url.fetch(function (data) { 61 | dataString = data.toString(); 62 | console.log(dataString); // show in console 63 | return dataString; 64 | }); 65 | }; 66 | 67 | return ApplicationLib; 68 | })(); 69 | _; 70 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/ApplicationLib.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2021 Taxyovio 3 | • isBeforeCurVers(versStrToCheck) // checks whether the provided version string comes before the current application version 4 | • isEqualToCurVers(versStrToCheck) // checks whether the provided version string is equal to the current application version 5 | • isAtLeastCurVers(versStrToCheck) // checks whether the provided version string is at least the current application version 6 | • isAfterCurVers(versStrToCheck) // checks whether the provided version string comes after the current application version -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/applyTitleCase.js: -------------------------------------------------------------------------------- 1 | // This action applies title case to the text of the selected rows. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | var selectedItems = selection.items; 7 | 8 | // List all visible text columns 9 | editor = document.editors[0]; 10 | filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | // Rename columns with the same titles 32 | filteredColumnTitles = renameStrings(filteredColumnTitles); 33 | filteredColumns.forEach((column, index) => { 34 | if (column.title !== "") { 35 | if (column.title !== filteredColumnTitles[index]) { 36 | column.title = filteredColumnTitles[index]; 37 | } 38 | } 39 | }); 40 | 41 | // CREATE FORM FOR GATHERING USER INPUT 42 | var inputForm = new Form(); 43 | 44 | // CREATE TEXT FIELD 45 | 46 | if (filteredColumns.includes(document.outline.outlineColumn)) { 47 | var defaultColumn = document.outline.outlineColumn; 48 | } else { 49 | var defaultColumn = document.outline.noteColumn; 50 | } 51 | 52 | var columnField = new Form.Field.Option( 53 | "columnInput", 54 | "Column", 55 | filteredColumns, 56 | filteredColumnTitles, 57 | defaultColumn 58 | ); 59 | 60 | // ADD THE FIELDS TO THE FORM 61 | inputForm.addField(columnField); 62 | // PRESENT THE FORM TO THE USER 63 | formPrompt = "Select Column"; 64 | formPromise = inputForm.show(formPrompt, "Continue"); 65 | 66 | // VALIDATE THE USER INPUT 67 | inputForm.validate = function (formObject) { 68 | return null; 69 | }; 70 | 71 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 72 | formPromise.then(function (formObject) { 73 | var selectedColumn = formObject.values["columnInput"]; 74 | 75 | selectedItems.forEach(function (item) { 76 | var textObj = item.valueForColumn(selectedColumn); 77 | var str = textObj.string; 78 | var sty = textObj.style; 79 | if (str.lastIndexOf("’") === -1) { 80 | newText = new Text(titleCaps(str), sty); 81 | item.setValueForColumn(newText, selectedColumn); 82 | } else { 83 | indices = []; 84 | for (var i = 0; i < str.length; i++) { 85 | if (str[i] === "’") indices.push(i); 86 | } 87 | str = str.replace(/’/g, "'"); 88 | str = titleCaps(str); 89 | indices.forEach(function (index) { 90 | str = setCharAt(str, index, "’"); 91 | }); 92 | newText = new Text(titleCaps(str), sty); 93 | item.setValueForColumn(newText, selectedColumn); 94 | } 95 | }); 96 | }); 97 | 98 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 99 | formPromise.catch(function (err) { 100 | console.log("form cancelled", err.message); 101 | }); 102 | }); 103 | 104 | action.validate = function (selection, sender) { 105 | // validation code 106 | // selection options: columns, document, editor, items, nodes, styles 107 | if (selection.items.length > 0) { 108 | return true; 109 | } else { 110 | return false; 111 | } 112 | }; 113 | 114 | return action; 115 | })(); 116 | 117 | function setCharAt(str, index, chr) { 118 | if (index > str.length - 1) return str; 119 | return str.substr(0, index) + chr + str.substr(index + 1); 120 | } 121 | 122 | function titleCaps(title) { 123 | // Ported to JavaScript By John Resig - http://ejohn.org/ - 21 May 2008 124 | // Original by John Gruber - http://daringfireball.net/ - 10 May 2008 125 | // License: http://www.opensource.org/licenses/mit-license.php 126 | 127 | var small = 128 | "(a|an|and|as|at|but|by|en|for|if|in|of|on|or|the|to|v[.]?|via|vs[.]?)"; 129 | var punct = "([!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]*)"; 130 | var parts = [], 131 | split = /[:.;?!] |(?: |^)["Ò]/g, 132 | index = 0; 133 | 134 | while (true) { 135 | var m = split.exec(title); 136 | 137 | parts.push( 138 | title 139 | .substring(index, m ? m.index : title.length) 140 | .replace(/\b([A-Za-z][a-z.'Õ]*)\b/g, function (all) { 141 | return /[A-Za-z]\.[A-Za-z]/.test(all) ? all : upper(all); 142 | }) 143 | .replace(RegExp("\\b" + small + "\\b", "ig"), lower) 144 | .replace( 145 | RegExp("^" + punct + small + "\\b", "ig"), 146 | function (all, punct, word) { 147 | return punct + upper(word); 148 | } 149 | ) 150 | .replace(RegExp("\\b" + small + punct + "$", "ig"), upper) 151 | ); 152 | 153 | index = split.lastIndex; 154 | 155 | if (m) parts.push(m[0]); 156 | else break; 157 | } 158 | 159 | return parts 160 | .join("") 161 | .replace(/ V(s?)\. /gi, " v$1. ") 162 | .replace(/(['Õ])S\b/gi, "$1s") 163 | .replace(/\b(AT&T|Q&A)\b/gi, function (all) { 164 | return all.toUpperCase(); 165 | }); 166 | } 167 | 168 | function lower(word) { 169 | return word.toLowerCase(); 170 | } 171 | 172 | function upper(word) { 173 | return word.substr(0, 1).toUpperCase() + word.substr(1); 174 | } 175 | 176 | function renameStrings(arr) { 177 | var count = {}; 178 | arr.forEach(function (x, i) { 179 | if (arr.indexOf(x) !== i) { 180 | var c = x in count ? (count[x] = count[x] + 1) : (count[x] = 1); 181 | var j = c + 1; 182 | var k = x + " " + j; 183 | while (arr.indexOf(k) !== -1) k = x + " " + ++j; 184 | arr[i] = k; 185 | } 186 | }); 187 | return arr; 188 | } 189 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/clearStyle.js: -------------------------------------------------------------------------------- 1 | // This action sets the style of selected rows to the base style of the document. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var textColumns = columns.filter((col) => { 7 | return col.type === Column.Type.Text; 8 | }); 9 | 10 | selection.items.forEach(function (item) { 11 | item.style.clear(); 12 | if (textColumns.length !== 0) { 13 | textColumns.forEach((col) => { 14 | try { 15 | item.valueForColumn(col).style.clear(); 16 | } catch (error) {} 17 | }); 18 | } 19 | }); 20 | }); 21 | 22 | action.validate = function (selection, sender) { 23 | // validation code 24 | // selection options: columns, document, editor, items, nodes, styles 25 | if (selection.items.length > 0) { 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | }; 31 | 32 | return action; 33 | })(); 34 | _; 35 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/applyTitleCase.strings: -------------------------------------------------------------------------------- 1 | "label" = "Apply Title Case"; 2 | "shortLabel" = "Title Case"; 3 | "mediumLabel" = "Title Case"; 4 | "LongLabel" = "Apply title case to selected rows"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/clearStyle.strings: -------------------------------------------------------------------------------- 1 | "label" = "Clear Style"; 2 | "shortLabel" = "Clear Style"; 3 | "mediumLabel" = "Clear Style"; 4 | "LongLabel" = "Set to base style"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/columnFormatter.strings: -------------------------------------------------------------------------------- 1 | "label" = "Column Formatter"; 2 | "shortLabel" = "Column"; 3 | "mediumLabel" = "Column Formatter"; 4 | "LongLabel" = "Format column of certain types"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/lineSpacing.strings: -------------------------------------------------------------------------------- 1 | "label" = "Line Spacing"; 2 | "shortLabel" = "Line Spacing"; 3 | "mediumLabel" = "Line Spacing"; 4 | "LongLabel" = "Set line spacing for selected rows"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.taxyovio.format" = "⓶ Format"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/pasteStyle.strings: -------------------------------------------------------------------------------- 1 | "label" = "Paste Style"; 2 | "shortLabel" = "Paste Style"; 3 | "mediumLabel" = "Paste Style"; 4 | "LongLabel" = "Paste style of selected row"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/splitParagraph.strings: -------------------------------------------------------------------------------- 1 | "label" = "Split Paragraph"; 2 | "shortLabel" = "Split Paragraph"; 3 | "mediumLabel" = "Split Paragraph"; 4 | "LongLabel" = "Split paragraphs in selected column into separate rows"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/en.lproj/trimColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Trim Column Title"; 2 | "shortLabel" = "Trim Column"; 3 | "mediumLabel" = "Trim Column Title"; 4 | "LongLabel" = "Trim all column titles"; -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taxyovio/OmniOutliner-Plug-Ins/bca08f8ae5409fcac616217c604b5cfcc5458dd9/format.omnioutlinerjs/Resources/icon.png -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/lineSpacing.js: -------------------------------------------------------------------------------- 1 | // This action sets the line spacing as a multiple of font size for the selected rows. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var items = selection.items; 7 | 8 | var fontSize = parseFloat( 9 | document.outline.baseStyle.get(Style.Attribute.FontSize).toString() 10 | ); 11 | var curSpacing = parseFloat( 12 | document.outline.baseStyle 13 | .get(Style.Attribute.ParagraphLineSpacing) 14 | .toString() 15 | ); 16 | var relativeHight = curSpacing / fontSize; 17 | console.log(fontSize, curSpacing, relativeHight); 18 | var inputForm = new Form(); 19 | 20 | // CREATE TEXT FIELD 21 | 22 | var spacingField = new Form.Field.String( 23 | "spacingInput", 24 | "Line Spacing", 25 | relativeHight.toString(), 26 | null 27 | ); 28 | 29 | // ADD THE FIELDS TO THE FORM 30 | inputForm.addField(spacingField); 31 | // PRESENT THE FORM TO THE USER 32 | formPrompt = "Enter Line Spacing:"; 33 | formPromise = inputForm.show(formPrompt, "Continue"); 34 | 35 | // VALIDATE THE USER INPUT 36 | inputForm.validate = function (formObject) { 37 | var float = parseFloat(formObject.values["spacingInput"]); 38 | if (isNaN(float) || float < 0) { 39 | throw new Error("Please enter a non-negative number."); 40 | return false; 41 | } else { 42 | return null; 43 | } 44 | }; 45 | 46 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 47 | formPromise.then(function (formObject) { 48 | var relativeHight = formObject.values["spacingInput"]; 49 | items.forEach((item) => { 50 | item.style.set( 51 | Style.Attribute.ParagraphLineSpacing, 52 | fontSize * relativeHight 53 | ); 54 | }); 55 | }); 56 | }); 57 | 58 | action.validate = function (selection, sender) { 59 | // validation code 60 | // selection options: columns, document, editor, items, nodes, styles 61 | if (selection.items.length > 0) { 62 | return true; 63 | } else { 64 | return false; 65 | } 66 | }; 67 | 68 | return action; 69 | })(); 70 | _; 71 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/pasteStyle.js: -------------------------------------------------------------------------------- 1 | // This action pastes the style of the selected row into the targets selected in the form. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var selectedItem = selection.items[0]; 7 | var rootItem = document.outline.rootItem; 8 | 9 | // Nowhere to paste if there's only one row 10 | if (rootItem.descendants.length <= 1) { 11 | throw new Error("No rows available for pasting."); 12 | } 13 | 14 | // Collect pasting targets 15 | var inputForm = new Form(); 16 | 17 | var allField = new Form.Field.Checkbox("allInput", "All", false); 18 | inputForm.addField(allField); 19 | 20 | if (selectedItem.hasChildren) { 21 | var defaultDescendants = false; 22 | var descendantsField = function (bool) { 23 | return new Form.Field.Checkbox( 24 | "descendantsInput", 25 | "Descendants", 26 | bool 27 | ); 28 | }; 29 | inputForm.addField(descendantsField(defaultDescendants)); 30 | 31 | var defaultChildren = false; 32 | var childrenField = function (bool) { 33 | return new Form.Field.Checkbox( 34 | "childrenInput", 35 | "Children", 36 | bool 37 | ); 38 | }; 39 | inputForm.addField(childrenField(defaultChildren)); 40 | 41 | var defaultLeaves = false; 42 | var leavesField = function (bool) { 43 | return new Form.Field.Checkbox("leavesInput", "Leaves", bool); 44 | }; 45 | inputForm.addField(leavesField(defaultLeaves)); 46 | } 47 | 48 | if (selectedItem.level > 1) { 49 | var defaultAncestors = false; 50 | var ancestorsField = function (bool) { 51 | return new Form.Field.Checkbox( 52 | "ancestorsInput", 53 | "Ancestors", 54 | bool 55 | ); 56 | }; 57 | inputForm.addField(ancestorsField(defaultAncestors)); 58 | 59 | var defaultParent = false; 60 | var parentField = function (bool) { 61 | return new Form.Field.Checkbox("parentInput", "Parent", bool); 62 | }; 63 | inputForm.addField(parentField(defaultParent)); 64 | } 65 | 66 | if (selectedItem.parent.children.length > 1) { 67 | // This means all following siblings and their descendants, basically everything below. 68 | var defaultFollowingCollateralDescendants = false; 69 | var followingCollateralDescendantsField = function (bool) { 70 | return new Form.Field.Checkbox( 71 | "followingCollateralDescendantsInput", 72 | "Following Collateral Descendants", 73 | bool 74 | ); 75 | }; 76 | inputForm.addField( 77 | followingCollateralDescendantsField( 78 | defaultFollowingCollateralDescendants 79 | ) 80 | ); 81 | 82 | var defaultFollowingSiblings = false; 83 | var followingSiblingsField = function (bool) { 84 | return new Form.Field.Checkbox( 85 | "followingSiblingsInput", 86 | "Following Siblings", 87 | bool 88 | ); 89 | }; 90 | inputForm.addField( 91 | followingSiblingsField(defaultFollowingSiblings) 92 | ); 93 | 94 | // Similarly this is everything above. 95 | var defaultPrecedingCollateralDescendants = false; 96 | var precedingCollateralDescendantsField = function (bool) { 97 | return new Form.Field.Checkbox( 98 | "precedingCollateralDescendantsInput", 99 | "Preceding Collateral Descendants", 100 | bool 101 | ); 102 | }; 103 | inputForm.addField( 104 | precedingCollateralDescendantsField( 105 | defaultPrecedingCollateralDescendants 106 | ) 107 | ); 108 | 109 | var defaultPrecedingSiblings = false; 110 | var precedingSiblingsField = function (bool) { 111 | return new Form.Field.Checkbox( 112 | "precedingSiblingsInput", 113 | "Preceding Siblings", 114 | bool 115 | ); 116 | }; 117 | inputForm.addField( 118 | precedingSiblingsField(defaultPrecedingSiblings) 119 | ); 120 | } 121 | 122 | // Declare field positions 123 | var descendantsInputIndex; 124 | var childrenInputIndex; 125 | var leavesInputIndex; 126 | var ancestorsInputIndex; 127 | var parentInputIndex; 128 | var followingCollateralDescendantsInputIndex; 129 | var precedingCollateralDescendantsInputIndex; 130 | var followingSiblingsInputIndex; 131 | var precedingSiblingsInputIndex; 132 | 133 | formPrompt = "Select Paste Target"; 134 | formPromise = inputForm.show(formPrompt, "Continue"); 135 | 136 | // VALIDATE THE USER INPUT 137 | inputForm.validate = function (formObject) { 138 | var keys = formObject.fields.map((field) => field.key); 139 | 140 | var allChosen = formObject.values["allInput"]; 141 | 142 | var descendantsChosen = formObject.values["descendantsInput"]; 143 | var childrenChosen = formObject.values["childrenInput"]; 144 | var leavesChosen = formObject.values["leavesInput"]; 145 | 146 | var ancestorsChosen = formObject.values["ancestorsInput"]; 147 | var parentChosen = formObject.values["parentInput"]; 148 | 149 | var followingCollateralDescendantsChosen = 150 | formObject.values["followingCollateralDescendantsInput"]; 151 | var precedingCollateralDescendantsChosen = 152 | formObject.values["precedingCollateralDescendantsInput"]; 153 | 154 | var followingSiblingsChosen = 155 | formObject.values["followingSiblingsInput"]; 156 | var precedingSiblingsChosen = 157 | formObject.values["precedingSiblingsInput"]; 158 | 159 | if (allChosen) { 160 | if (keys.indexOf("precedingSiblingsInput") !== -1) { 161 | defaultPrecedingSiblings = precedingSiblingsChosen; 162 | precedingSiblingsInputIndex = keys.indexOf( 163 | "precedingSiblingsInput" 164 | ); 165 | formObject.removeField( 166 | formObject.fields[precedingSiblingsInputIndex] 167 | ); 168 | } 169 | 170 | if (keys.indexOf("followingSiblingsInput") !== -1) { 171 | defaultFollowingSiblings = followingSiblingsChosen; 172 | followingSiblingsInputIndex = keys.indexOf( 173 | "followingSiblingsInput" 174 | ); 175 | formObject.removeField( 176 | formObject.fields[followingSiblingsInputIndex] 177 | ); 178 | } 179 | 180 | if ( 181 | keys.indexOf("precedingCollateralDescendantsInput") !== -1 182 | ) { 183 | defaultPrecedingCollateralDescendants = 184 | precedingCollateralDescendantsChosen; 185 | precedingCollateralDescendantsInputIndex = keys.indexOf( 186 | "precedingCollateralDescendantsInput" 187 | ); 188 | formObject.removeField( 189 | formObject.fields[ 190 | precedingCollateralDescendantsInputIndex 191 | ] 192 | ); 193 | } 194 | 195 | if ( 196 | keys.indexOf("followingCollateralDescendantsInput") !== -1 197 | ) { 198 | defaultFollowingCollateralDescendants = 199 | followingCollateralDescendantsChosen; 200 | followingCollateralDescendantsInputIndex = keys.indexOf( 201 | "followingCollateralDescendantsInput" 202 | ); 203 | formObject.removeField( 204 | formObject.fields[ 205 | followingCollateralDescendantsInputIndex 206 | ] 207 | ); 208 | } 209 | 210 | if (keys.indexOf("parentInput") !== -1) { 211 | defaultParent = parentChosen; 212 | parentInputIndex = keys.indexOf("parentInput"); 213 | formObject.removeField(formObject.fields[parentInputIndex]); 214 | } 215 | 216 | if (keys.indexOf("ancestorsInput") !== -1) { 217 | defaultAncestors = ancestorsChosen; 218 | ancestorsInputIndex = keys.indexOf("ancestorsInput"); 219 | formObject.removeField( 220 | formObject.fields[ancestorsInputIndex] 221 | ); 222 | } 223 | 224 | if (keys.indexOf("leavesInput") !== -1) { 225 | defaultLeaves = leavesChosen; 226 | leavesInputIndex = keys.indexOf("leavesInput"); 227 | formObject.removeField(formObject.fields[leavesInputIndex]); 228 | } 229 | 230 | if (keys.indexOf("childrenInput") !== -1) { 231 | defaultChildren = childrenChosen; 232 | childrenInputIndex = keys.indexOf("childrenInput"); 233 | formObject.removeField( 234 | formObject.fields[childrenInputIndex] 235 | ); 236 | } 237 | 238 | if (keys.indexOf("descendantsInput") !== -1) { 239 | defaultDescendants = descendantsChosen; 240 | descendantsInputIndex = keys.indexOf("descendantsInput"); 241 | formObject.removeField( 242 | formObject.fields[descendantsInputIndex] 243 | ); 244 | } 245 | } else { 246 | if (selectedItem.hasChildren) { 247 | if (keys.indexOf("descendantsInput") === -1) { 248 | formObject.addField( 249 | descendantsField(defaultDescendants), 250 | descendantsInputIndex 251 | ); 252 | } 253 | } 254 | 255 | if (selectedItem.level > 1) { 256 | if (keys.indexOf("ancestorsInput") === -1) { 257 | formObject.addField( 258 | ancestorsField(defaultAncestors), 259 | ancestorsInputIndex 260 | ); 261 | } 262 | } 263 | 264 | if (selectedItem.parent.children.length > 1) { 265 | if ( 266 | keys.indexOf("followingCollateralDescendantsInput") === 267 | -1 268 | ) { 269 | formObject.addField( 270 | followingCollateralDescendantsField( 271 | defaultFollowingCollateralDescendants 272 | ), 273 | followingCollateralDescendantsInputIndex 274 | ); 275 | } 276 | 277 | if ( 278 | keys.indexOf("precedingCollateralDescendantsInput") === 279 | -1 280 | ) { 281 | formObject.addField( 282 | precedingCollateralDescendantsField( 283 | defaultPrecedingCollateralDescendants 284 | ), 285 | precedingCollateralDescendantsInputIndex 286 | ); 287 | } 288 | } 289 | 290 | if (descendantsChosen) { 291 | if (keys.indexOf("leavesInput") !== -1) { 292 | defaultLeaves = leavesChosen; 293 | leavesInputIndex = keys.indexOf("leavesInput"); 294 | formObject.removeField( 295 | formObject.fields[leavesInputIndex] 296 | ); 297 | } 298 | 299 | if (keys.indexOf("childrenInput") !== -1) { 300 | defaultChildren = childrenChosen; 301 | childrenInputIndex = keys.indexOf("childrenInput"); 302 | formObject.removeField( 303 | formObject.fields[childrenInputIndex] 304 | ); 305 | } 306 | } else if (descendantsChosen === false) { 307 | if (keys.indexOf("childrenInput") === -1) { 308 | formObject.addField( 309 | childrenField(defaultChildren), 310 | childrenInputIndex 311 | ); 312 | } 313 | 314 | if (keys.indexOf("leavesInput") === -1) { 315 | formObject.addField( 316 | leavesField(defaultLeaves), 317 | leavesInputIndex 318 | ); 319 | } 320 | } 321 | 322 | if (ancestorsChosen) { 323 | if (keys.indexOf("parentInput") !== -1) { 324 | defaultParent = parentChosen; 325 | parentInputIndex = keys.indexOf("parentInput"); 326 | formObject.removeField( 327 | formObject.fields[parentInputIndex] 328 | ); 329 | } 330 | } else if (ancestorsChosen === false) { 331 | if (keys.indexOf("parentInput") === -1) { 332 | formObject.addField( 333 | parentField(defaultParent), 334 | parentInputIndex 335 | ); 336 | } 337 | } 338 | 339 | if (followingCollateralDescendantsChosen) { 340 | if (keys.indexOf("followingSiblingsInput") !== -1) { 341 | defaultFollowingSiblings = followingSiblingsChosen; 342 | followingSiblingsInputIndex = keys.indexOf( 343 | "followingSiblingsInput" 344 | ); 345 | console.log( 346 | "before removing", 347 | followingSiblingsInputIndex 348 | ); 349 | formObject.removeField( 350 | formObject.fields[followingSiblingsInputIndex] 351 | ); 352 | } 353 | } else if (followingCollateralDescendantsChosen === false) { 354 | if (keys.indexOf("followingSiblingsInput") === -1) { 355 | console.log( 356 | "before adding", 357 | followingSiblingsInputIndex 358 | ); 359 | formObject.addField( 360 | followingSiblingsField(defaultFollowingSiblings), 361 | followingSiblingsInputIndex 362 | ); 363 | } 364 | } 365 | 366 | if (precedingCollateralDescendantsChosen) { 367 | if (keys.indexOf("precedingSiblingsInput") !== -1) { 368 | defaultPrecedingSiblings = precedingSiblingsChosen; 369 | precedingSiblingsInputIndex = keys.indexOf( 370 | "precedingSiblingsInput" 371 | ); 372 | formObject.removeField( 373 | formObject.fields[precedingSiblingsInputIndex] 374 | ); 375 | } 376 | } else if (precedingCollateralDescendantsChosen === false) { 377 | if (keys.indexOf("precedingSiblingsInput") === -1) { 378 | formObject.addField( 379 | precedingSiblingsField(defaultPrecedingSiblings), 380 | precedingSiblingsInputIndex 381 | ); 382 | } 383 | } 384 | } 385 | 386 | return null; 387 | }; 388 | 389 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 390 | formPromise.then(function (formObject) { 391 | var sourceStyle = selectedItem.style; 392 | 393 | var allChosen = formObject.values["allInput"]; 394 | 395 | var descendantsChosen = formObject.values["descendantsInput"]; 396 | var childrenChosen = formObject.values["childrenInput"]; 397 | var leavesChosen = formObject.values["leavesInput"]; 398 | 399 | var ancestorsChosen = formObject.values["ancestorsInput"]; 400 | var parentChosen = formObject.values["parentInput"]; 401 | 402 | var followingCollateralDescendantsChosen = 403 | formObject.values["followingCollateralDescendantsInput"]; 404 | var precedingCollateralDescendantsChosen = 405 | formObject.values["precedingCollateralDescendantsInput"]; 406 | 407 | var followingSiblingsChosen = 408 | formObject.values["followingSiblingsInput"]; 409 | var precedingSiblingsChosen = 410 | formObject.values["precedingSiblingsInput"]; 411 | 412 | if (allChosen) { 413 | rootItem.descendants.forEach((item) => { 414 | item.style.setStyle(sourceStyle); 415 | }); 416 | } else { 417 | if (descendantsChosen) { 418 | selectedItem.descendants.forEach((item) => { 419 | item.style.setStyle(sourceStyle); 420 | }); 421 | } 422 | 423 | if (!descendantsChosen && childrenChosen) { 424 | selectedItem.children.forEach((item) => { 425 | item.style.setStyle(sourceStyle); 426 | }); 427 | } 428 | 429 | if (!descendantsChosen && leavesChosen) { 430 | selectedItem.leaves.forEach((item) => { 431 | item.style.setStyle(sourceStyle); 432 | }); 433 | } 434 | 435 | if (ancestorsChosen) { 436 | selectedItem.ancestors.forEach((item) => { 437 | item.style.setStyle(sourceStyle); 438 | }); 439 | } 440 | 441 | if (!ancestorsChosen && parentChosen) { 442 | selectedItem.parent.style.setStyle(sourceStyle); 443 | } 444 | 445 | if (followingCollateralDescendantsChosen) { 446 | selectedItem.followingSiblings.forEach((item) => { 447 | item.style.setStyle(sourceStyle); 448 | item.descendants.forEach((des) => { 449 | des.style.setStyle(sourceStyle); 450 | }); 451 | }); 452 | } 453 | 454 | if (precedingCollateralDescendantsChosen) { 455 | selectedItem.precedingSiblings.forEach((item) => { 456 | item.style.setStyle(sourceStyle); 457 | item.descendants.forEach((des) => { 458 | des.style.setStyle(sourceStyle); 459 | }); 460 | }); 461 | } 462 | 463 | if ( 464 | !followingCollateralDescendantsChosen && 465 | followingSiblingsChosen 466 | ) { 467 | selectedItem.followingSiblings.forEach((item) => { 468 | item.style.setStyle(sourceStyle); 469 | }); 470 | } 471 | 472 | if ( 473 | !precedingCollateralDescendantsChosen && 474 | precedingSiblingsChosen 475 | ) { 476 | selectedItem.precedingSiblings.forEach((item) => { 477 | item.style.setStyle(sourceStyle); 478 | }); 479 | } 480 | } 481 | }); 482 | 483 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 484 | formPromise.catch(function (err) { 485 | console.log("form cancelled", err.message); 486 | }); 487 | }); 488 | 489 | action.validate = function (selection, sender) { 490 | // validation code 491 | // selection options: columns, document, editor, items, nodes, styles 492 | if (selection.items.length === 1) { 493 | return true; 494 | } else { 495 | return false; 496 | } 497 | }; 498 | 499 | return action; 500 | })(); 501 | _; 502 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/splitParagraph.js: -------------------------------------------------------------------------------- 1 | // This action splits the texts in the selected column according to paragraphs for the selected rows. Note that the selected node will be turned into the last paragraoh. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var editor = document.editors[0]; 7 | var selectedItems = selection.items; 8 | var pb = Pasteboard.makeUnique(); 9 | 10 | // List all visible text columns 11 | var filteredColumns = columns.filter(function (column) { 12 | if (editor.visibilityOfColumn(column)) { 13 | if (column.type === Column.Type.Text) { 14 | return column; 15 | } 16 | } 17 | }); 18 | 19 | if (filteredColumns.length === 0) { 20 | throw new Error("This document has no visible text columns."); 21 | } 22 | 23 | var filteredColumnTitles = filteredColumns.map(function (column) { 24 | if (column.title !== "") { 25 | return column.title; 26 | } else if (column === document.outline.noteColumn) { 27 | // The note column is the only text column with empty title 28 | return "Notes"; 29 | } 30 | }); 31 | 32 | // CREATE FORM FOR GATHERING USER INPUT 33 | var inputForm = new Form(); 34 | 35 | // CREATE TEXT FIELD 36 | 37 | if (filteredColumns.includes(document.outline.outlineColumn)) { 38 | var defaultColumn = document.outline.outlineColumn; 39 | } else { 40 | var defaultColumn = document.outline.noteColumn; 41 | } 42 | 43 | var columnField = new Form.Field.Option( 44 | "columnInput", 45 | "Column", 46 | filteredColumns, 47 | filteredColumnTitles, 48 | defaultColumn 49 | ); 50 | 51 | // ADD THE FIELDS TO THE FORM 52 | inputForm.addField(columnField); 53 | // PRESENT THE FORM TO THE USER 54 | formPrompt = "Select Column"; 55 | formPromise = inputForm.show(formPrompt, "Continue"); 56 | 57 | // VALIDATE THE USER INPUT 58 | inputForm.validate = function (formObject) { 59 | return null; 60 | }; 61 | 62 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 63 | formPromise.then(function (formObject) { 64 | var selectedColumn = formObject.values["columnInput"]; 65 | var selectedColumnTitle = selectedColumn.title; 66 | 67 | // Rename columns with the same titles temporarily to work around bugs in Timer and Tree.paste 68 | if (hasDuplicates(filteredColumnTitles)) { 69 | var duplicateColumns = { columns: [], titles: [], indices: [] }; 70 | filteredColumnTitles = renameStrings(filteredColumnTitles); 71 | filteredColumns.forEach((column, index) => { 72 | if (column.title !== "") { 73 | if (column.title !== filteredColumnTitles[index]) { 74 | duplicateColumns.columns.push(column); 75 | duplicateColumns.titles.push(column.title); 76 | // Adding the index of the column in all columns, not filtered columns 77 | duplicateColumns.indices.push( 78 | columns.indexOf(column) 79 | ); 80 | column.title = filteredColumnTitles[index]; 81 | } 82 | } 83 | }); 84 | var duplicateColumnTitles = duplicateColumns.titles; 85 | var duplicateColumnIndices = duplicateColumns.indices; 86 | } 87 | 88 | editor 89 | .nodesForObjects(selectedItems) 90 | .forEach(function (node, index) { 91 | // Copy node to pasteboard 92 | editor.copyNodes([node], pb); 93 | 94 | // Get the text object from selected column 95 | var textObj = node.valueForColumn(selectedColumn); 96 | var paragraphArray = textObj.paragraphs; 97 | var paragraphStringArray = paragraphArray.map(function ( 98 | par 99 | ) { 100 | if (par.string && /\S/.test(par.string)) { 101 | return par.string.trim(); 102 | } 103 | }); 104 | var paragraphStringArray = paragraphStringArray.filter( 105 | (el) => { 106 | return el; 107 | } 108 | ); 109 | 110 | var paragraphArrayLength = paragraphStringArray.length; 111 | 112 | if (paragraphArrayLength > 1) { 113 | // Prepare timer to work around Tree.paste bug 114 | var counter = 0; 115 | var repeats = paragraphArrayLength; 116 | 117 | Timer.repeating(0.05, function (timer) { 118 | var childIndex = node.index; 119 | 120 | if (counter === repeats) { 121 | if (duplicateColumnTitles) { 122 | duplicateColumnTitles.forEach( 123 | (title, i) => { 124 | columns[ 125 | duplicateColumnIndices[i] 126 | ].title = title; 127 | } 128 | ); 129 | } 130 | console.log("done"); 131 | timer.cancel(); 132 | } else if (counter === 0) { 133 | console.log("counter: ", counter); 134 | node.object.valueForColumn( 135 | selectedColumn 136 | ).string = paragraphStringArray 137 | .slice() 138 | .reverse()[0]; 139 | counter = counter + 1; 140 | } else { 141 | console.log("counter: ", counter); 142 | 143 | editor.paste( 144 | pb, 145 | node.parent, 146 | childIndex + 1 - counter 147 | ); 148 | var newNode = 149 | node.parent.children[ 150 | childIndex + 1 - counter 151 | ]; 152 | if ( 153 | paragraphStringArray.slice().reverse()[ 154 | counter 155 | ] 156 | ) { 157 | newNode.object.valueForColumn( 158 | selectedColumn 159 | ).string = paragraphStringArray 160 | .slice() 161 | .reverse()[counter]; 162 | } 163 | counter = counter + 1; 164 | } 165 | }); 166 | } else { 167 | alert = new Alert( 168 | "Error", 169 | "Not enough paragraphs are found." 170 | ); 171 | alert.show(); 172 | } 173 | }); 174 | }); 175 | 176 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 177 | formPromise.catch(function (err) { 178 | console.log("form cancelled", err.message); 179 | }); 180 | }); 181 | 182 | action.validate = function (selection, sender) { 183 | // validation code 184 | // selection options: columns, document, editor, items, nodes, styles 185 | if (selection.items.length > 0) { 186 | return true; 187 | } else { 188 | return false; 189 | } 190 | }; 191 | 192 | return action; 193 | })(); 194 | _; 195 | 196 | function renameStrings(arr) { 197 | var count = {}; 198 | arr.forEach(function (x, i) { 199 | if (arr.indexOf(x) !== i) { 200 | var c = x in count ? (count[x] = count[x] + 1) : (count[x] = 1); 201 | var j = c + 1; 202 | var k = x + " ".repeat(j); 203 | while (arr.indexOf(k) !== -1) k = x + " " + ++j; 204 | arr[i] = k; 205 | } 206 | }); 207 | return arr; 208 | } 209 | 210 | function hasDuplicates(array) { 211 | return new Set(array).size !== array.length; 212 | } 213 | 214 | function columnByTitle(columnArray, title) { 215 | for (var i = 0; i < columnArray.length; i++) { 216 | if (columnArray[i].title === title) { 217 | return columnArray[i]; 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/Resources/trimColumn.js: -------------------------------------------------------------------------------- 1 | // This action trimes all column titles except Status and Notes. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | var trimmableColumns = columns.filter((col) => { 7 | return col !== noteColumn && col !== statusColumn; 8 | }); 9 | var count = 0; 10 | trimmableColumns.forEach((col) => { 11 | if (col.title !== col.title.trim()) { 12 | count += 1; 13 | col.title = col.title.trim(); 14 | } 15 | }); 16 | var alertTitle = "Confirmation"; 17 | if (count === 0) { 18 | var alertMessage = "No column title has been trimmed."; 19 | } else if (count === 1) { 20 | var alertMessage = "1 column title has been trimmed."; 21 | } else { 22 | var alertMessage = count + " column titles have been trimmed."; 23 | } 24 | 25 | var alert = new Alert(alertTitle, alertMessage); 26 | alert.show(); 27 | }); 28 | 29 | action.validate = function (selection, sender) { 30 | // selection options: columns, document, editor, items, nodes, styles 31 | if (document) { 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | }; 37 | 38 | return action; 39 | })(); 40 | -------------------------------------------------------------------------------- /format.omnioutlinerjs/manifest.json: -------------------------------------------------------------------------------- 1 | {"author":"Taxyovio","libraries":[{"identifier":"ApplicationLib"}],"identifier":"com.taxyovio.format","version":"2021.03.17","description":"A collection of Omni Automation scripts formatting data in OmniOutliner.","actions":[ 2 | {"image":"a.square","identifier":"pasteStyle"}, 3 | {"image":"character.textbox","identifier":"clearStyle"}, 4 | {"image":"line.3.horizontal","identifier":"lineSpacing"}, 5 | {"image":"number.square","identifier":"columnFormatter"}, 6 | {"image":"textformat","identifier":"applyTitleCase"}, 7 | {"image":"paragraphsign","identifier":"splitParagraph"}, 8 | {"image":"delete.right","identifier":"trimColumn"}, 9 | ],"defaultLocale":"en"} -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/ApplicationLib.js: -------------------------------------------------------------------------------- 1 | var _ = (function () { 2 | var ApplicationLib = new PlugIn.Library(new Version("1.0")); 3 | 4 | ApplicationLib.isBeforeCurVers = function (versStrToCheck) { 5 | curVersStr = app.version; 6 | curVers = new Version(curVersStr); 7 | versToCheck = new Version(versStrToCheck); 8 | result = versToCheck.isBefore(curVers); 9 | console.log( 10 | versStrToCheck + " is before " + curVersStr + " = " + result 11 | ); 12 | return result; 13 | }; 14 | 15 | ApplicationLib.isEqualToCurVers = function (versStrToCheck) { 16 | curVersStr = app.version; 17 | curVers = new Version(curVersStr); 18 | versToCheck = new Version(versStrToCheck); 19 | result = versToCheck.equals(curVers); 20 | console.log(versStrToCheck + " equals " + curVersStr + " = " + result); 21 | return result; 22 | }; 23 | 24 | ApplicationLib.isAtLeastCurVers = function (versStrToCheck) { 25 | curVersStr = app.version; 26 | curVers = new Version(curVersStr); 27 | versToCheck = new Version(versStrToCheck); 28 | result = versToCheck.atLeast(curVers); 29 | console.log( 30 | versStrToCheck + " is at least " + curVersStr + " = " + result 31 | ); 32 | return result; 33 | }; 34 | 35 | ApplicationLib.isAfterCurVers = function (versStrToCheck) { 36 | curVersStr = app.version; 37 | curVers = new Version(curVersStr); 38 | versToCheck = new Version(versStrToCheck); 39 | result = versToCheck.isAfter(curVers); 40 | console.log( 41 | versStrToCheck + " is after " + curVersStr + " = " + result 42 | ); 43 | return result; 44 | }; 45 | 46 | // returns a list of functions 47 | ApplicationLib.handlers = function () { 48 | return "\n// ApplicationLib ©2021 Taxyovio\n• isBeforeCurVers(versStrToCheck)\n• isEqualToCurVers(versStrToCheck)\n• isAtLeastCurVers(versStrToCheck)\n• isAfterCurVers(versStrToCheck)"; 49 | }; 50 | 51 | // returns contents of matching strings file 52 | ApplicationLib.documentation = function () { 53 | // create a version object 54 | var aVersion = new Version("1.0"); 55 | // look up the plugin 56 | var plugin = PlugIn.find("com.Taxyovio.OmniOutliner", aVersion); 57 | // get the url for the text file inside this plugin 58 | var url = plugin.resourceNamed("ApplicationLib.strings"); 59 | // read the file 60 | url.fetch(function (data) { 61 | dataString = data.toString(); 62 | console.log(dataString); // show in console 63 | return dataString; 64 | }); 65 | }; 66 | 67 | // text object to markdown 68 | ApplicationLib.textToMD = function (textObj) { 69 | var str = ""; 70 | var runs = textObj.attributeRuns; 71 | var hasAttachment = !(textObj.attachments.length === 0); 72 | 73 | const imgExts = ["png", "jpeg", "jpg", "bmp", "tiff"]; 74 | 75 | runs.forEach((text) => { 76 | if (text.style.link) { 77 | // Check for hyperlink 78 | var urlStr = text.style.link.string; 79 | 80 | // Check for possible images 81 | var filename = urlStr.substr(urlStr.lastIndexOf("/") + 1); 82 | 83 | if (filename) { 84 | var baseName = filename.substring( 85 | 0, 86 | filename.lastIndexOf(".") 87 | ); 88 | var extension = filename.substring( 89 | filename.lastIndexOf(".") + 1, 90 | filename.length 91 | ); 92 | // If there's no extension in the filename, the above codes assign the whole name to extension. 93 | if (baseName === "") { 94 | baseName = extension; 95 | extension = ""; 96 | } 97 | 98 | if (imgExts.indexOf(extension) !== -1) { 99 | str += "![" + baseName + "](" + urlStr + ")"; 100 | } else { 101 | if (urlStr === text.string) { 102 | str += "<" + urlStr + ">"; 103 | } else { 104 | str += "[" + text.string + "](" + urlStr + ")"; 105 | } 106 | } 107 | } else { 108 | console.log("hi"); 109 | if (urlStr === text.string) { 110 | str += "<" + urlStr + ">"; 111 | } else { 112 | str += "[" + text.string + "](" + urlStr + ")"; 113 | } 114 | } 115 | } else if (text.fileWrapper) { 116 | // Check for attachments and if they are images 117 | var filename = text.fileWrapper.preferredFilename; 118 | var baseName = filename.substring(0, filename.lastIndexOf(".")); 119 | var extension = filename.substring( 120 | filename.lastIndexOf(".") + 1, 121 | filename.length 122 | ); 123 | 124 | // If there's no extension in the filename, the above codes assign the whole name to extension. 125 | if (baseName === "") { 126 | baseName = extension; 127 | extension = ""; 128 | } 129 | 130 | if (imgExts.indexOf(extension) !== -1) { 131 | str += " ![" + baseName + "](" + filename + ") "; 132 | } else { 133 | str += " <" + filename + "> "; 134 | } 135 | } else { 136 | str += text.string; 137 | } 138 | }); 139 | console.log(textObj.string, "->\n", str); 140 | return str; 141 | }; 142 | return ApplicationLib; 143 | })(); 144 | _; 145 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/ApplicationLib.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2021 Taxyovio 3 | • isBeforeCurVers(versStrToCheck) // checks whether the provided version string comes before the current application version 4 | • isEqualToCurVers(versStrToCheck) // checks whether the provided version string is equal to the current application version 5 | • isAtLeastCurVers(versStrToCheck) // checks whether the provided version string is at least the current application version 6 | • isAfterCurVers(versStrToCheck) // checks whether the provided version string comes after the current application version 7 | • textToMD(textObj) // converts the text object into markdown -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/addToAnki.js: -------------------------------------------------------------------------------- 1 | // This action requires using the 'Add to Anki' otemplate and appropriate configuration for card types in Anki. Three custom card types are defined with custom fields: {Basic: Front, Back, Reference, Reverse, Extra}, {Cloze: Text, Reference, Extra}, {Input: Front, Back, Reference, Extra}. 2 | 3 | // This action creates a new note in Anki Mobile from the selected row using URL schemes. 4 | var _ = (function () { 5 | var action = new PlugIn.Action(function (selection, sender) { 6 | // action code 7 | var columns = document.outline.columns; 8 | if ( 9 | !columns.byTitle("Deck") || 10 | !columns.byTitle("Front") || 11 | !columns.byTitle("Type") 12 | ) { 13 | var alertTitle = "Confirmation"; 14 | var alertMessage = 15 | "Missing required columns.\nCreate a new template document?"; 16 | var alert = new Alert(alertTitle, alertMessage); 17 | alert.addOption("Cancel"); 18 | alert.addOption("Continue"); 19 | var alertPromise = alert.show(); 20 | 21 | alertPromise.then((buttonIndex) => { 22 | if (buttonIndex === 1) { 23 | console.log("Continue script"); 24 | createNewAnkiTemplate(); 25 | } else { 26 | throw new Error("script cancelled"); 27 | } 28 | }); 29 | } else { 30 | // This is the delay between each url call, constrained by the app switching animation on iOS. 31 | // If it's set to too low, there could be data loss on transport. 32 | // Set default delay to 1 sec on iOS devices unless there's only one selected item 33 | if (app.platformName === "iOS" || app.platformName === "iPadOS") { 34 | if (selection.items.length === 1) { 35 | var delay = 0; 36 | } else { 37 | var delay = 1; 38 | } 39 | } else { 40 | var delay = 0; 41 | } 42 | console.log("Default delay is set to", delay, "seconds."); 43 | 44 | // CREATE FORM FOR GATHERING USER INPUT 45 | var inputForm = new Form(); 46 | 47 | if (Pasteboard.general.hasStrings) { 48 | var defaultProfile = Pasteboard.general.string; 49 | } else { 50 | var defaultProfile = ""; 51 | } 52 | 53 | // CREATE TEXT FIELD 54 | var profileField = new Form.Field.String( 55 | "profileInput", 56 | "Profile", 57 | defaultProfile 58 | ); 59 | 60 | // ADD THE FIELDS TO THE FORM 61 | inputForm.addField(profileField); 62 | 63 | // Delay is nonzero if and only if there're multiple selected items on iOS. 64 | // This field gives the option to remove this delay when multitasking. 65 | if (delay !== 0) { 66 | var multitaskingField = new Form.Field.Checkbox( 67 | "multitaskingInput", 68 | "Multitasking", 69 | false 70 | ); 71 | inputForm.addField(multitaskingField); 72 | } 73 | 74 | inputForm.validate = function (formObject) { 75 | if ( 76 | !formObject.values["profileInput"] || 77 | !formObject.values["profileInput"].trim() 78 | ) { 79 | throw new Error("Profile name is invalid."); 80 | } 81 | return null; 82 | }; 83 | 84 | // PRESENT THE FORM TO THE USER 85 | formPrompt = "Add to Anki"; 86 | formPromise = inputForm.show(formPrompt, "Continue"); 87 | 88 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 89 | formPromise.then(function (formObject) { 90 | var profile = formObject.values["profileInput"]; 91 | // This field is undefined unless selecting multiple rows on iOS. 92 | try { 93 | var multitasking = formObject.values["multitaskingInput"]; 94 | } catch (err) { 95 | multitasking = false; 96 | } 97 | 98 | // Constructing array of urls to call 99 | var urls = []; 100 | selection.items.forEach(function (item) { 101 | try { 102 | var front = item.topic; 103 | } catch (err) { 104 | var front = ""; 105 | } 106 | try { 107 | var back = item.valueForColumn( 108 | columns.byTitle("Back") 109 | ).string; 110 | } catch (err) { 111 | var back = ""; 112 | } 113 | try { 114 | var deck = item.valueForColumn( 115 | columns.byTitle("Deck") 116 | ).string; 117 | } catch (err) { 118 | var deck = "Default"; 119 | } 120 | try { 121 | var type = item.valueForColumn( 122 | columns.byTitle("Type") 123 | ).name; 124 | } catch (err) { 125 | var type = "Basic"; 126 | } 127 | try { 128 | var tags = item.valueForColumn( 129 | columns.byTitle("Tags") 130 | ).string; 131 | } catch (err) { 132 | var tags = ""; 133 | } 134 | try { 135 | var reference = item.valueForColumn( 136 | columns.byTitle("Reference") 137 | ).string; 138 | } catch (err) { 139 | var reference = ""; 140 | } 141 | try { 142 | var extra = item.valueForColumn( 143 | columns.byTitle("Extra") 144 | ).string; 145 | } catch (err) { 146 | var extra = ""; 147 | } 148 | try { 149 | var reverse = item.valueForColumn( 150 | columns.byTitle("Reverse") 151 | ).name; 152 | } catch (err) { 153 | var reverse = ""; 154 | } 155 | 156 | if (front != "") { 157 | front = encodeURIComponent(front); 158 | back = encodeURIComponent(back); 159 | deck = encodeURIComponent(deck); 160 | type = encodeURIComponent(type); 161 | tags = encodeURIComponent(tags); 162 | if (type === "Basic" || type === "") { 163 | urlStr = 164 | "anki://x-callback-url/addnote?profile=" + 165 | profile + 166 | "&type=" + 167 | type + 168 | "&deck=" + 169 | deck + 170 | "&fldFront=" + 171 | front + 172 | "&fldBack=" + 173 | back + 174 | "&tags=" + 175 | tags; 176 | if (reverse === "Yes") { 177 | urlStr = urlStr + "&fldReverse=Y"; 178 | } 179 | } else if (type === "Cloze") { 180 | urlStr = 181 | "anki://x-callback-url/addnote?profile=" + 182 | profile + 183 | "&type=" + 184 | type + 185 | "&deck=" + 186 | deck + 187 | "&fldText=" + 188 | front + 189 | "&tags=" + 190 | tags; 191 | } else if (type === "Input") { 192 | urlStr = 193 | "anki://x-callback-url/addnote?profile=" + 194 | profile + 195 | "&type=" + 196 | type + 197 | "&deck=" + 198 | deck + 199 | "&fldFront=" + 200 | front + 201 | "&fldBack=" + 202 | back + 203 | "&tags=" + 204 | tags; 205 | } 206 | 207 | if (reference !== "") { 208 | urlStr = 209 | urlStr + 210 | "&fldReference=" + 211 | encodeURIComponent(reference); 212 | } 213 | if (extra !== "") { 214 | urlStr = 215 | urlStr + 216 | "&fldExtra=" + 217 | encodeURIComponent(extra); 218 | } 219 | urls.push(URL.fromString(urlStr)); 220 | } 221 | }); 222 | 223 | // Call the urls 224 | // Warn against flagging on multitasking as true 225 | if (multitasking === true) { 226 | var alertTitle = "Warning"; 227 | var alertMessage = 228 | "Anki and OmniOutliner must be on the same screen.\nContinue with multitasking?"; 229 | var alert = new Alert(alertTitle, alertMessage); 230 | alert.addOption("Cancel"); 231 | alert.addOption("Continue"); 232 | var alertPromise = alert.show(); 233 | 234 | alertPromise.then((buttonIndex) => { 235 | if (buttonIndex === 1) { 236 | console.log("Continue without delay"); 237 | repeatingCall(urls, 0); 238 | } else { 239 | console.log("Continue with delay"); 240 | repeatingCall(urls, delay); 241 | } 242 | }); 243 | } else { 244 | console.log("Continue with delay"); 245 | repeatingCall(urls, delay); 246 | } 247 | }); 248 | 249 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 250 | formPromise.catch(function (err) { 251 | console.log("form cancelled", err.message); 252 | }); 253 | } 254 | }); 255 | 256 | action.validate = function (selection, sender) { 257 | // validation code 258 | // selection options: columns, document, editor, items, nodes, styles 259 | if (selection.items.length > 0) { 260 | return true; 261 | } else { 262 | return false; 263 | } 264 | }; 265 | 266 | return action; 267 | })(); 268 | _; 269 | 270 | function createNewAnkiTemplate() { 271 | Document.makeNew(function (doc) { 272 | outline = doc.outline; 273 | editor = doc.editors[0]; 274 | outline.outlineColumn.title = "Front"; 275 | outline.addColumn( 276 | Column.Type.Text, 277 | editor.afterColumn(outline.outlineColumn), 278 | function (column) { 279 | column.title = "Back"; 280 | } 281 | ); 282 | outline.addColumn( 283 | Column.Type.Text, 284 | editor.afterColumn(null), 285 | function (column) { 286 | column.title = "Tags"; 287 | } 288 | ); 289 | outline.addColumn( 290 | Column.Type.Text, 291 | editor.afterColumn(null), 292 | function (column) { 293 | column.title = "Reference"; 294 | } 295 | ); 296 | outline.addColumn( 297 | Column.Type.Text, 298 | editor.afterColumn(null), 299 | function (column) { 300 | column.title = "Extra"; 301 | } 302 | ); 303 | outline.addColumn( 304 | Column.Type.Enumeration, 305 | editor.afterColumn(null), 306 | function (column) { 307 | column.title = "Reverse"; 308 | column.enumeration.add("No"); 309 | column.enumeration.add("Yes"); 310 | } 311 | ); 312 | 313 | outline.addColumn( 314 | Column.Type.Text, 315 | editor.beforeColumn(outline.outlineColumn), 316 | function (column) { 317 | column.title = "Deck"; 318 | } 319 | ); 320 | outline.addColumn( 321 | Column.Type.Enumeration, 322 | editor.beforeColumn(outline.outlineColumn), 323 | function (column) { 324 | column.title = "Type"; 325 | column.enumeration.add("Basic"); 326 | column.enumeration.add("Cloze"); 327 | column.enumeration.add("Input"); 328 | } 329 | ); 330 | baseItem = editor.rootNode.object; 331 | baseItem.addChild(baseItem.beginning, function (item) { 332 | item.topic = "This is the front text of a input card."; 333 | item.setValueForColumn( 334 | outline.columns 335 | .byTitle("Type") 336 | .enumeration.memberNamed("Input"), 337 | outline.columns.byTitle("Type") 338 | ); 339 | item.setValueForColumn("Academia", outline.columns.byTitle("Deck")); 340 | item.setValueForColumn( 341 | "This is the back text for a input card.", 342 | outline.columns.byTitle("Back") 343 | ); 344 | item.setValueForColumn( 345 | "tag1 tag2", 346 | outline.columns.byTitle("Tags") 347 | ); 348 | item.setValueForColumn( 349 | "Wikipedia", 350 | outline.columns.byTitle("Reference") 351 | ); 352 | item.setValueForColumn( 353 | "Extra notes", 354 | outline.columns.byTitle("Extra") 355 | ); 356 | }); 357 | baseItem.addChild(baseItem.beginning, function (item) { 358 | item.topic = "This is the {{c1::text}} of a {{c2::cloze card}}."; 359 | item.setValueForColumn( 360 | outline.columns 361 | .byTitle("Type") 362 | .enumeration.memberNamed("Cloze"), 363 | outline.columns.byTitle("Type") 364 | ); 365 | item.setValueForColumn("Academia", outline.columns.byTitle("Deck")); 366 | item.setValueForColumn("N/A", outline.columns.byTitle("Back")); 367 | item.setValueForColumn( 368 | "tag1 tag2", 369 | outline.columns.byTitle("Tags") 370 | ); 371 | item.setValueForColumn( 372 | "Wikipedia", 373 | outline.columns.byTitle("Reference") 374 | ); 375 | item.setValueForColumn( 376 | "Extra notes", 377 | outline.columns.byTitle("Extra") 378 | ); 379 | }); 380 | baseItem.addChild(baseItem.beginning, function (item) { 381 | item.topic = "This is the front text of a basic card."; 382 | item.setValueForColumn( 383 | outline.columns 384 | .byTitle("Type") 385 | .enumeration.memberNamed("Basic"), 386 | outline.columns.byTitle("Type") 387 | ); 388 | item.setValueForColumn("Academia", outline.columns.byTitle("Deck")); 389 | item.setValueForColumn( 390 | "This is the back text for a basic card.", 391 | outline.columns.byTitle("Back") 392 | ); 393 | item.setValueForColumn( 394 | "tag1 tag2", 395 | outline.columns.byTitle("Tags") 396 | ); 397 | item.setValueForColumn( 398 | outline.columns 399 | .byTitle("Reverse") 400 | .enumeration.memberNamed("No"), 401 | outline.columns.byTitle("Reverse") 402 | ); 403 | item.setValueForColumn( 404 | "Wikipedia", 405 | outline.columns.byTitle("Reference") 406 | ); 407 | item.setValueForColumn( 408 | "Extra notes", 409 | outline.columns.byTitle("Extra") 410 | ); 411 | }); 412 | baseItem.addChild(baseItem.beginning, function (item) { 413 | item.topic = 414 | 'This is a template for the "Add to Anki" Omni Automation action. This action requires using appropriate configuration for card types in Anki. Three custom card types are defined with custom fields: {Basic: Front, Back, Reference, Reverse, Extra}, {Cloze: Text, Reference, Extra}, {Input: Front, Back, Reference, Extra}.'; 415 | }); 416 | doc.save(); 417 | 418 | if (document) { 419 | var alertTitle = "Confirmation"; 420 | var alertMessage = 421 | doc.name + 422 | ".ooutline has been created.\nClose current document?"; 423 | var alert = new Alert(alertTitle, alertMessage); 424 | alert.addOption("Done"); 425 | alert.addOption("Close"); 426 | var alertPromise = alert.show(); 427 | 428 | alertPromise.then((buttonIndex) => { 429 | if (buttonIndex === 1) { 430 | console.log("Continue script"); 431 | document.close(); 432 | } else { 433 | throw new Error("script cancelled"); 434 | } 435 | }); 436 | } else { 437 | var alertTitle = "Confirmation"; 438 | var alertMessage = doc.name + ".ooutline has been created."; 439 | var alert = new Alert(alertTitle, alertMessage); 440 | var alertPromise = alert.show(); 441 | } 442 | }); 443 | } 444 | 445 | // Minimal delay 0.8 for mutiple tasks if app switching is needed. 446 | function repeatingCall(urls, delay) { 447 | console.log("Calling URLs: ", urls); 448 | var counter = 0; 449 | var repeats = urls.length; 450 | 451 | // In the Timer, all user defined objects get invalidated. 452 | // Usable objects: document, columns, rootItem, outlineColumn, noteColumn, statusColumn 453 | Timer.repeating(delay, function (timer) { 454 | if (counter === repeats) { 455 | console.log("done"); 456 | timer.cancel(); 457 | return "Complete"; 458 | } else { 459 | console.log("counter: ", counter); 460 | counter = counter + 1; 461 | urls[counter - 1].call(function (result) { 462 | console.log("result", result); 463 | }); 464 | } 465 | }); 466 | } 467 | 468 | function columnByTitle(columnArray, title) { 469 | for (var i = 0; i < columnArray.length; i++) { 470 | if (columnArray[i].title === title) { 471 | return columnArray[i]; 472 | } 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/addToDEVONthink.js: -------------------------------------------------------------------------------- 1 | // This action creates a new text file in DEVONthink To Go from the selected row using URL schemes. Document name is passed as text title. Topic is passed as text body. Note is passed as comment. Item link is passed as URL. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | const Lib = this.plugIn.library("ApplicationLib"); 6 | // selection options: columns, document, editor, items, nodes, styles 7 | 8 | const editor = document.editors[0]; 9 | 10 | // This is the delay between each url call, constrained by the app switching animation on iOS. 11 | // If it's set to too low, there could be data loss on transport. 12 | // Set default delay to 1 sec on iOS devices unless there's only one selected item 13 | if (app.platformName === "iOS" || app.platformName === "iPadOS") { 14 | if (selection.items.length === 1) { 15 | var delay = 0; 16 | } else { 17 | var delay = 1; 18 | } 19 | } else { 20 | var delay = 0; 21 | } 22 | console.log("Default delay is set to", delay, "seconds."); 23 | 24 | // Create DEVONthink ID column 25 | if ( 26 | !columns.byTitle("DEVONthink ID") || 27 | columns.byTitle("DEVONthink ID").type !== Column.Type.Text 28 | ) { 29 | document.outline.addColumn( 30 | Column.Type.Text, 31 | editor.afterColumn(), 32 | function (column) { 33 | column.title = "DEVONthink ID"; 34 | } 35 | ); 36 | } 37 | 38 | // Creating urls 39 | var urls = []; 40 | selection.items.forEach(function (item) { 41 | try { 42 | title = document.name; 43 | } catch (err) { 44 | title = ""; 45 | } 46 | try { 47 | text = Lib.textToMD(item.valueForColumn(outlineColumn)); 48 | } catch (err) { 49 | text = ""; 50 | } 51 | try { 52 | comment = Lib.textToMD(item.valueForColumn(noteColumn)); 53 | } catch (err) { 54 | comment = ""; 55 | } 56 | itemLink = "omnioutliner:///open?row=" + item.identifier; 57 | itemLink = encodeURIComponent(itemLink); 58 | title = encodeURIComponent(title); 59 | if (text != "") { 60 | text = encodeURIComponent(text); 61 | urlStr = 62 | "x-devonthink://x-callback-url/createtext?title=" + 63 | title + 64 | "&text=" + 65 | text + 66 | "&location=" + 67 | itemLink; 68 | if (comment != "") { 69 | comment = encodeURIComponent(comment); 70 | urlStr += "&comment=" + comment; 71 | } 72 | urls.push(URL.fromString(urlStr)); 73 | } 74 | }); 75 | 76 | // Call the urls 77 | // Warn against flagging on multitasking as true 78 | if (selection.items.length > 1) { 79 | var alertTitle = "Confirmation"; 80 | var alertMessage = 81 | "The process can be accerelated if DEVONthink and OmniOutliner are on the same screen.\nContinue with multitasking?"; 82 | var alert = new Alert(alertTitle, alertMessage); 83 | alert.addOption("No"); 84 | alert.addOption("Continue"); 85 | var alertPromise = alert.show(); 86 | 87 | alertPromise.then((buttonIndex) => { 88 | if (buttonIndex === 1) { 89 | console.log("Continue without delay"); 90 | repeatingCall(urls, 0); 91 | } else { 92 | console.log("Continue with delay"); 93 | repeatingCall(urls, delay); 94 | } 95 | }); 96 | } else { 97 | console.log("Continue without delay"); 98 | if (urls.length == 1) { 99 | urls[0].call(function (result) { 100 | console.log("result", JSON.stringify(result)); 101 | var urlStr = result.itemlink; 102 | var str = urlStr.replace(/x-devonthink-item:\/\//, ""); 103 | var url = URL.fromString(urlStr); 104 | var item = document.editors[0].selection.items[0]; 105 | 106 | var textColumns = columns.filter(function (column) { 107 | if (column.type === Column.Type.Text) { 108 | return column; 109 | } 110 | }); 111 | 112 | var urlColumn = columnByTitle(textColumns, "DEVONthink ID"); 113 | 114 | var textObj = item.valueForColumn(urlColumn); 115 | if (textObj) { 116 | textObj = new Text(str, textObj.style); 117 | textObj.style.set(Style.Attribute.Link, url); 118 | item.setValueForColumn(textObj, urlColumn); 119 | } else { 120 | textObj = new Text(str, item.style); 121 | textObj.style.set(Style.Attribute.Link, url); 122 | item.setValueForColumn(textObj, urlColumn); 123 | } 124 | }); 125 | } else { 126 | repeatingCall(urls, 0); 127 | } 128 | } 129 | }); 130 | 131 | action.validate = function (selection, sender) { 132 | // validation code 133 | // selection options: columns, document, editor, items, nodes, styles 134 | if (selection.items.length > 0) { 135 | return true; 136 | } else { 137 | return false; 138 | } 139 | }; 140 | 141 | return action; 142 | })(); 143 | _; 144 | 145 | // Minimal delay 0.8 for mutiple tasks if app switching is needed. 146 | function repeatingCall(urls, delay) { 147 | console.log("Calling URLs: ", urls); 148 | var counter = 0; 149 | var repeats = urls.length; 150 | 151 | // In the Timer, all user defined objects get invalidated. 152 | // Usable objects: document, columns, rootItem, outlineColumn, noteColumn, statusColumn 153 | function callBack() { 154 | urls[counter].call(function (result) { 155 | console.log("result", JSON.stringify(result)); 156 | var urlStr = result.itemlink; 157 | var str = urlStr.replace(/x-devonthink-item:\/\//, ""); 158 | var url = URL.fromString(urlStr); 159 | var item = document.editors[0].selection.items[counter - 1]; 160 | 161 | var textColumns = columns.filter(function (column) { 162 | if (column.type === Column.Type.Text) { 163 | return column; 164 | } 165 | }); 166 | 167 | var urlColumn = columnByTitle(textColumns, "DEVONthink ID"); 168 | 169 | var textObj = item.valueForColumn(urlColumn); 170 | if (textObj) { 171 | textObj = new Text(str, textObj.style); 172 | textObj.style.set(Style.Attribute.Link, url); 173 | item.setValueForColumn(textObj, urlColumn); 174 | } else { 175 | textObj = new Text(str, item.style); 176 | textObj.style.set(Style.Attribute.Link, url); 177 | item.setValueForColumn(textObj, urlColumn); 178 | } 179 | }); 180 | } 181 | 182 | // Timer.repeating is bugged and causing crashes when cencelling. Write my own repeating timer as a workaround. 183 | function repeatingTimer(interval, func) { 184 | console.log("counter", counter); 185 | func(); 186 | counter = counter + 1; 187 | if (counter < repeats) { 188 | Timer.once(interval, function (timer) { 189 | repeatingTimer(interval, func); 190 | }); 191 | } 192 | } 193 | 194 | repeatingTimer(delay, callBack); 195 | } 196 | 197 | function columnByTitle(columnArray, title) { 198 | for (var i = 0; i < columnArray.length; i++) { 199 | if (columnArray[i].title === title) { 200 | return columnArray[i]; 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/addToAnki.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add to Anki"; 2 | "shortLabel" = "Anki"; 3 | "mediumLabel" = "Add to Anki"; 4 | "LongLabel" = "Add the selected item to Anki"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/addToDEVONthink.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add to DEVONthink"; 2 | "shortLabel" = "DEVONthink"; 3 | "mediumLabel" = "Add to DEVONthink"; 4 | "LongLabel" = "Add selected item to DEVONthink"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/addToThings.strings: -------------------------------------------------------------------------------- 1 | "label" = "Add to Things"; 2 | "shortLabel" = "Things"; 3 | "mediumLabel" = "Add to Things"; 4 | "LongLabel" = "Add to selected item to Things"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.taxyovio.share" = "⓸ Share"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/shareAsMarkdown.strings: -------------------------------------------------------------------------------- 1 | "label" = "Share as Markdown"; 2 | "shortLabel" = "Markdown"; 3 | "mediumLabel" = "Share as Markdown"; 4 | "longLabel" = "Share the current document as markdown"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/shareAttachment.strings: -------------------------------------------------------------------------------- 1 | "label" = "Share Attachment"; 2 | "shortLabel" = "Share Attachment"; 3 | "mediumLabel" = "Share Attachment"; 4 | "LongLabel" = "Share attachments from selected rows"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/shareClipboard.strings: -------------------------------------------------------------------------------- 1 | "label" = "Share Clipboard"; 2 | "shortLabel" = "Share Clipboard"; 3 | "mediumLabel" = "Share Clipboard"; 4 | "LongLabel" = "Share clipboard contents"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/shareColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Share Column"; 2 | "shortLabel" = "Share Column"; 3 | "mediumLabel" = "Share Column"; 4 | "LongLabel" = "Share column texts from selected rows"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/en.lproj/shareItemLink.strings: -------------------------------------------------------------------------------- 1 | "label" = "Share as Link"; 2 | "shortLabel" = "Share Link"; 3 | "mediumLabel" = "Share Link"; 4 | "LongLabel" = "Share item links from selected items"; -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taxyovio/OmniOutliner-Plug-Ins/bca08f8ae5409fcac616217c604b5cfcc5458dd9/share.omnioutlinerjs/Resources/icon.png -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/shareAsMarkdown.js: -------------------------------------------------------------------------------- 1 | // This action presents the selected rows in chosen column as markdown file in Share Sheet. 2 | // If there's no header markup (#) in any rows of the document in the chosen column, then the export adds # according to level to every selected row. Otherwise, only rows with # are exported with # according to level, while others are exported as body texts. 3 | var _ = (function () { 4 | var action = new PlugIn.Action(function (selection, sender) { 5 | const Lib = this.plugIn.library("ApplicationLib"); 6 | var selectedItems = selection.items; 7 | 8 | // List all visible text columns 9 | var editor = document.editors[0]; 10 | var filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | var filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | // CREATE FORM FOR GATHERING USER INPUT 32 | var inputForm = new Form(); 33 | 34 | // CREATE TEXT FIELD 35 | 36 | if (filteredColumns.includes(document.outline.outlineColumn)) { 37 | var defaultColumn = document.outline.outlineColumn; 38 | } else { 39 | var defaultColumn = document.outline.noteColumn; 40 | } 41 | 42 | var columnField = new Form.Field.Option( 43 | "columnInput", 44 | "Column", 45 | filteredColumns, 46 | filteredColumnTitles, 47 | defaultColumn 48 | ); 49 | 50 | // ADD THE FIELDS TO THE FORM 51 | inputForm.addField(columnField); 52 | // PRESENT THE FORM TO THE USER 53 | formPrompt = "Select Column"; 54 | formPromise = inputForm.show(formPrompt, "Continue"); 55 | 56 | // VALIDATE THE USER INPUT 57 | inputForm.validate = function (formObject) { 58 | return null; 59 | }; 60 | 61 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 62 | formPromise.then(function (formObject) { 63 | var selectedColumn = formObject.values["columnInput"]; 64 | var strings = []; 65 | // Get strings from all top level rows to see if there are markdown header markups 66 | rootItem.children.forEach(function (item) { 67 | if (selectedColumn === document.outline.outlineColumn) { 68 | strings.push(item.topic); 69 | } else if (selectedColumn === document.outline.noteColumn) { 70 | strings.push(item.note); 71 | } else { 72 | if (item.valueForColumn(selectedColumn)) { 73 | strings.push( 74 | item.valueForColumn(selectedColumn).string 75 | ); 76 | } 77 | } 78 | }); 79 | 80 | if (!hasHash(strings)) { 81 | strings = []; 82 | selectedItems.forEach(function (item) { 83 | var level = item.level; 84 | 85 | if (item.valueForColumn(selectedColumn)) { 86 | strings.push( 87 | "#".repeat(level) + 88 | " " + 89 | Lib.textToMD( 90 | item.valueForColumn(selectedColumn) 91 | ).replace(/^\s*/, "") 92 | ); 93 | } else { 94 | strings.push(""); 95 | } 96 | }); 97 | } else { 98 | strings = []; 99 | selectedItems.forEach(function (item) { 100 | var level = item.level; 101 | 102 | if (item.valueForColumn(selectedColumn)) { 103 | if ( 104 | /^#/.test( 105 | item.valueForColumn(selectedColumn).string 106 | ) 107 | ) { 108 | strings.push( 109 | "#".repeat(level) + 110 | " " + 111 | Lib.textToMD( 112 | item.valueForColumn(selectedColumn) 113 | ).replace(/^#+\s*/, "") 114 | ); 115 | } else { 116 | strings.push( 117 | Lib.textToMD( 118 | item.valueForColumn(selectedColumn) 119 | ).replace(/^\s*/, "") 120 | ); 121 | } 122 | } else { 123 | strings.push(""); 124 | } 125 | }); 126 | } 127 | 128 | var data = Data.fromString(strings.join("\n")); 129 | var fileWrapper = FileWrapper.withContents( 130 | document.name + ".md", 131 | data 132 | ); 133 | sharePanel = new SharePanel([fileWrapper]); 134 | sharePanel.show(); 135 | }); 136 | 137 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 138 | formPromise.catch(function (err) { 139 | console.log("form cancelled", err.message); 140 | }); 141 | }); 142 | 143 | action.validate = function (selection, sender) { 144 | if (selection.items.length > 0) { 145 | return true; 146 | } else { 147 | return false; 148 | } 149 | }; 150 | return action; 151 | })(); 152 | _; 153 | 154 | function hasHash(arr) { 155 | for (var i = 0; i < arr.length; i++) { 156 | if (/^#/.test(arr[i])) { 157 | return true; 158 | } 159 | } 160 | return false; 161 | } 162 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/shareAttachment.js: -------------------------------------------------------------------------------- 1 | // This action presents the Topic texts from selected rows in Share Sheet. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | var textColumns = columns.filter(function (column) { 8 | if (column.type === Column.Type.Text) { 9 | return column; 10 | } 11 | }); 12 | 13 | if (textColumns.length === 0) { 14 | throw new Error("This document has no text columns."); 15 | } 16 | var wrappers = []; 17 | selection.items.forEach((item) => { 18 | textColumns.forEach((column) => { 19 | var textObj = item.valueForColumn(column); 20 | 21 | if (textObj) { 22 | // Get attachments 23 | var attachments = textObj.attachments; 24 | 25 | if (attachments) { 26 | attachments.forEach((att) => { 27 | var wrapper = att.fileWrapper; 28 | // We only want to rename files, not directories nor symbolic links 29 | if (wrapper.type === FileWrapper.Type.File) { 30 | wrappers.push(wrapper); 31 | } 32 | }); 33 | } 34 | } 35 | }); 36 | }); 37 | 38 | if (wrappers.length > 0) { 39 | sharePanel = new SharePanel(wrappers); 40 | sharePanel.show(); 41 | } else { 42 | var alertTitle = "Error"; 43 | var alertMessage = "No attachment found."; 44 | var alert = new Alert(alertTitle, alertMessage); 45 | var alertPromise = alert.show(); 46 | } 47 | }); 48 | 49 | action.validate = function (selection, sender) { 50 | // validation code 51 | // selection options: columns, document, editor, items, nodes, styles 52 | if (selection.items.length > 0) { 53 | return true; 54 | } else { 55 | return false; 56 | } 57 | }; 58 | 59 | return action; 60 | })(); 61 | _; 62 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/shareClipboard.js: -------------------------------------------------------------------------------- 1 | // This action presents the clipboard contents in Share Sheet. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | var array = []; 8 | 9 | Pasteboard.general.items.forEach((obj, index) => { 10 | var types = obj.types.filter((type) => { 11 | return ( 12 | type.identifier !== 13 | "com.omnigroup.omnioutliner.pboard.xmloutline.items" && 14 | type.identifier !== 15 | "com.omnigroup.omnioutliner.pboard.items-v3" && 16 | type.identifier !== "com.omnigroup.omnistyle.pboard.xml" && 17 | type.identifier !== 18 | "com.apple.DocumentManager.uti.FPItem.File" 19 | ); 20 | }); 21 | 22 | // Construct new text obj 23 | if (types.length !== 0) { 24 | console.log( 25 | "clipboard object ", 26 | index + 1, 27 | "/", 28 | Pasteboard.general.items.length, 29 | "is of type", 30 | types 31 | ); 32 | 33 | var textTypes = types.filter((type) => { 34 | return ( 35 | type.conformsTo(TypeIdentifier.plainText) || 36 | type.identifier === "public.utf8-plain-text" || 37 | type === TypeIdentifier.URL 38 | ); 39 | }); 40 | 41 | if (textTypes.length !== 0) { 42 | var str = ""; 43 | if ( 44 | textTypes.some((type) => 45 | type.conformsTo(TypeIdentifier.plainText) 46 | ) 47 | ) { 48 | str = obj.stringForType( 49 | types.find((type) => 50 | type.conformsTo(TypeIdentifier.plainText) 51 | ) 52 | ); 53 | } else if ( 54 | obj.types.some( 55 | (type) => 56 | type.identifier === "public.utf8-plain-text" 57 | ) 58 | ) { 59 | str = obj.stringForType( 60 | types.find( 61 | (type) => 62 | type.identifier === "public.utf8-plain-text" 63 | ) 64 | ); 65 | } else if ( 66 | obj.types.some((type) => type === TypeIdentifier.URL) 67 | ) { 68 | str = obj.stringForType( 69 | types.find((type) => type === TypeIdentifier.URL) 70 | ); 71 | } 72 | array.push(str.trim()); 73 | } 74 | 75 | var nonTextTypes = types.filter((type) => { 76 | return ( 77 | !type.conformsTo(TypeIdentifier.plainText) && 78 | type.identifier !== "public.utf8-plain-text" && 79 | type.identifier !== "public.url" 80 | ); 81 | }); 82 | 83 | if (nonTextTypes.length !== 0) { 84 | nonTextTypes.forEach((type, i) => { 85 | var fileExtension = type.pathExtensions[0]; 86 | if (fileExtension) { 87 | var fileName = 88 | fileExtension.toUpperCase() + 89 | " " + 90 | index.toString() + 91 | i.toString() + 92 | "." + 93 | fileExtension; 94 | } else { 95 | var fileName = index.toString(); 96 | } 97 | var newWrapper = FileWrapper.withContents( 98 | fileName, 99 | obj.dataForType(type) 100 | ); 101 | array.push(newWrapper); 102 | }); 103 | } 104 | } else { 105 | console.log( 106 | "clipboard object ", 107 | index + 1, 108 | "/", 109 | Pasteboard.general.items.length, 110 | "has no valid type" 111 | ); 112 | } 113 | }); 114 | 115 | var sharePanel = new SharePanel(array); 116 | sharePanel.show(); 117 | }); 118 | 119 | action.validate = function (selection, sender) { 120 | // validation code 121 | // selection options: columns, document, editor, items, nodes, styles 122 | if (Pasteboard.general.items.length > 0) { 123 | var typeCount = 0; 124 | Pasteboard.general.items.forEach((obj, index) => { 125 | var types = obj.types.filter((type) => { 126 | return ( 127 | type.identifier !== 128 | "com.omnigroup.omnioutliner.pboard.xmloutline.items" && 129 | type.identifier !== 130 | "com.omnigroup.omnioutliner.pboard.items-v3" && 131 | type.identifier !== 132 | "com.omnigroup.omnistyle.pboard.xml" && 133 | type.identifier !== 134 | "com.apple.DocumentManager.uti.FPItem.File" 135 | ); 136 | }); 137 | typeCount += types.length; 138 | }); 139 | if (typeCount !== 0) { 140 | return true; 141 | } else { 142 | return false; 143 | } 144 | } else { 145 | return false; 146 | } 147 | }; 148 | 149 | return action; 150 | })(); 151 | _; 152 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/shareColumn.js: -------------------------------------------------------------------------------- 1 | // This action presents the texts from selected column from selected rows in Share Sheet. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | const Lib = this.plugIn.library("ApplicationLib"); 6 | // selection options: columns, document, editor, items, nodes, styles 7 | var columnTitles = columns.map(function (column) { 8 | if (column.title !== "") { 9 | return column.title; 10 | } else if (column === document.outline.noteColumn) { 11 | // The note column is the only text column with empty title 12 | return "Notes"; 13 | } else if (column === document.outline.statusColumn) { 14 | // The note column is the only text column with empty title 15 | return "Status"; 16 | } 17 | }); 18 | 19 | // CREATE FORM FOR GATHERING USER INPUT 20 | var inputForm = new Form(); 21 | 22 | // CREATE TEXT FIELD 23 | var columnField = new Form.Field.Option( 24 | "columnInput", 25 | "Column", 26 | columns, 27 | columnTitles, 28 | document.outline.outlineColumn 29 | ); 30 | inputForm.addField(columnField); 31 | 32 | if (selection.items.length > 1) { 33 | var arrayToggle = new Form.Field.Checkbox( 34 | "arrayToggleInput", 35 | "Share as Array", 36 | false 37 | ); 38 | inputForm.addField(arrayToggle); 39 | } 40 | // PRESENT THE FORM TO THE USER 41 | formPrompt = "Share Column"; 42 | formPromise = inputForm.show(formPrompt, "Continue"); 43 | 44 | // VALIDATE THE USER INPUT 45 | inputForm.validate = function (formObject) { 46 | return null; 47 | }; 48 | 49 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 50 | formPromise.then(function (formObject) { 51 | var col = formObject.values["columnInput"]; 52 | var shareAsArray = formObject.values["arrayToggleInput"]; 53 | var strings = selection.items.map((item) => { 54 | var value = item.valueForColumn(col); 55 | /* 56 | All column types: 57 | var Checkbox → Column.Type read-only 58 | var Date → Column.Type read-only 59 | var Duration → Column.Type read-only 60 | var Enumeration → Column.Type read-only 61 | var Number → Column.Type read-only 62 | var Text → Column.Type read-only 63 | */ 64 | 65 | if (col.type === Column.Type.Text) { 66 | // Value is a Text object 67 | if (value) { 68 | return Lib.textToMD(value); 69 | } else { 70 | return "\n"; 71 | } 72 | } else if (col.type === Column.Type.Checkbox) { 73 | // Value is a State object 74 | if (value === State.Checked) { 75 | return "checked"; 76 | } else if (value === State.Unchecked) { 77 | return "unchecked"; 78 | } else if (value === State.Mixed) { 79 | return "mixed"; 80 | } else { 81 | return "\n"; 82 | } 83 | } else if (col.type === Column.Type.Date) { 84 | // Value is a Date object 85 | if (value) { 86 | return value.toString(); 87 | } else { 88 | return "\n"; 89 | } 90 | } else if ( 91 | col.type === Column.Type.Duration || 92 | col.type === Column.Type.Number 93 | ) { 94 | // Value is a Decimal object 95 | if (value) { 96 | return value.toString(); 97 | } else { 98 | return "\n"; 99 | } 100 | } else if (col.type === Column.Type.Enumeration) { 101 | // Value is an Enumaration object 102 | if (value) { 103 | return value.name.trim(); 104 | } else { 105 | return "\n"; 106 | } 107 | } 108 | }); 109 | if (shareAsArray) { 110 | var sharePanel = new SharePanel(strings); 111 | } else { 112 | var sharePanel = new SharePanel([strings.join("\n")]); 113 | } 114 | 115 | sharePanel.show(); 116 | }); 117 | 118 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 119 | formPromise.catch(function (err) { 120 | console.log("form cancelled", err.message); 121 | }); 122 | }); 123 | 124 | action.validate = function (selection, sender) { 125 | // validation code 126 | // selection options: columns, document, editor, items, nodes, styles 127 | if (selection.items.length > 0) { 128 | return true; 129 | } else { 130 | return false; 131 | } 132 | }; 133 | 134 | return action; 135 | })(); 136 | _; 137 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/Resources/shareItemLink.js: -------------------------------------------------------------------------------- 1 | // This action presents the item links from selected rows in Share Sheet. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | if (selection.items.length > 1) { 8 | // CREATE FORM FOR GATHERING USER INPUT 9 | var inputForm = new Form(); 10 | 11 | var arrayToggle = new Form.Field.Checkbox( 12 | "arrayToggleInput", 13 | "Share as Array", 14 | false 15 | ); 16 | 17 | inputForm.addField(arrayToggle); 18 | // PRESENT THE FORM TO THE USER 19 | formPrompt = "Share as Link"; 20 | formPromise = inputForm.show(formPrompt, "Continue"); 21 | 22 | // VALIDATE THE USER INPUT 23 | inputForm.validate = function (formObject) { 24 | return null; 25 | }; 26 | 27 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 28 | formPromise.then(function (formObject) { 29 | var shareAsArray = formObject.values["arrayToggleInput"]; 30 | 31 | linksArray = []; 32 | selection.items.forEach(function (item) { 33 | itemLink = "omnioutliner:///open?row=" + item.identifier; 34 | linksArray.push(itemLink); 35 | }); 36 | 37 | if (shareAsArray) { 38 | var sharePanel = new SharePanel(linksArray); 39 | } else { 40 | var sharePanel = new SharePanel([linksArray.join("\n")]); 41 | } 42 | sharePanel.show(); 43 | }); 44 | 45 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 46 | formPromise.catch(function (err) { 47 | console.log("form cancelled", err.message); 48 | }); 49 | } else { 50 | itemLink = 51 | "omnioutliner:///open?row=" + selection.items[0].identifier; 52 | var sharePanel = new SharePanel([itemLink]); 53 | sharePanel.show(); 54 | } 55 | }); 56 | 57 | action.validate = function (selection, sender) { 58 | // validation code 59 | // selection options: columns, document, editor, items, nodes, styles 60 | if (selection.items.length > 0) { 61 | return true; 62 | } else { 63 | return false; 64 | } 65 | }; 66 | 67 | return action; 68 | })(); 69 | _; 70 | -------------------------------------------------------------------------------- /share.omnioutlinerjs/manifest.json: -------------------------------------------------------------------------------- 1 | {"author":"Taxyovio","libraries":[{"identifier":"ApplicationLib"}],"identifier":"com.taxyovio.share","version":"2021.03.17","description":"A collection of Omni Automation scripts exporting data out of OmniOutliner.","actions":[ 2 | {"image":"note.text","identifier":"addToAnki"}, 3 | {"image":"checklist","identifier":"addToThings"}, 4 | {"image":"doc.richtext","identifier":"addToDEVONthink"}, 5 | {"image":"chart.bar.doc.horizontal","identifier":"shareColumn"}, 6 | {"image":"paperclip.circle","identifier":"shareAttachment"}, 7 | {"image":"link.circle","identifier":"shareItemLink"}, 8 | {"image":"arrow.down.doc","identifier":"shareAsMarkdown"}, 9 | {"image":"arrow.up.doc.on.clipboard","identifier":"shareClipboard"}, 10 | ],"defaultLocale":"en"} -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/ApplicationLib.js: -------------------------------------------------------------------------------- 1 | var _ = (function () { 2 | var ApplicationLib = new PlugIn.Library(new Version("1.0")); 3 | 4 | ApplicationLib.isBeforeCurVers = function (versStrToCheck) { 5 | curVersStr = app.version; 6 | curVers = new Version(curVersStr); 7 | versToCheck = new Version(versStrToCheck); 8 | result = versToCheck.isBefore(curVers); 9 | console.log( 10 | versStrToCheck + " is before " + curVersStr + " = " + result 11 | ); 12 | return result; 13 | }; 14 | 15 | ApplicationLib.isEqualToCurVers = function (versStrToCheck) { 16 | curVersStr = app.version; 17 | curVers = new Version(curVersStr); 18 | versToCheck = new Version(versStrToCheck); 19 | result = versToCheck.equals(curVers); 20 | console.log(versStrToCheck + " equals " + curVersStr + " = " + result); 21 | return result; 22 | }; 23 | 24 | ApplicationLib.isAtLeastCurVers = function (versStrToCheck) { 25 | curVersStr = app.version; 26 | curVers = new Version(curVersStr); 27 | versToCheck = new Version(versStrToCheck); 28 | result = versToCheck.atLeast(curVers); 29 | console.log( 30 | versStrToCheck + " is at least " + curVersStr + " = " + result 31 | ); 32 | return result; 33 | }; 34 | 35 | ApplicationLib.isAfterCurVers = function (versStrToCheck) { 36 | curVersStr = app.version; 37 | curVers = new Version(curVersStr); 38 | versToCheck = new Version(versStrToCheck); 39 | result = versToCheck.isAfter(curVers); 40 | console.log( 41 | versStrToCheck + " is after " + curVersStr + " = " + result 42 | ); 43 | return result; 44 | }; 45 | 46 | // returns a list of functions 47 | ApplicationLib.handlers = function () { 48 | return "\n// ApplicationLib ©2021 Taxyovio\n• isBeforeCurVers(versStrToCheck)\n• isEqualToCurVers(versStrToCheck)\n• isAtLeastCurVers(versStrToCheck)\n• isAfterCurVers(versStrToCheck)"; 49 | }; 50 | 51 | // returns contents of matching strings file 52 | ApplicationLib.documentation = function () { 53 | // create a version object 54 | var aVersion = new Version("1.0"); 55 | // look up the plugin 56 | var plugin = PlugIn.find("com.Taxyovio.OmniOutliner", aVersion); 57 | // get the url for the text file inside this plugin 58 | var url = plugin.resourceNamed("ApplicationLib.strings"); 59 | // read the file 60 | url.fetch(function (data) { 61 | dataString = data.toString(); 62 | console.log(dataString); // show in console 63 | return dataString; 64 | }); 65 | }; 66 | 67 | return ApplicationLib; 68 | })(); 69 | _; 70 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/ApplicationLib.strings: -------------------------------------------------------------------------------- 1 | 2 | // ApplicationLib ©2021 Taxyovio 3 | • isBeforeCurVers(versStrToCheck) // checks whether the provided version string comes before the current application version 4 | • isEqualToCurVers(versStrToCheck) // checks whether the provided version string is equal to the current application version 5 | • isAtLeastCurVers(versStrToCheck) // checks whether the provided version string is at least the current application version 6 | • isAfterCurVers(versStrToCheck) // checks whether the provided version string comes after the current application version -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/characterCount.js: -------------------------------------------------------------------------------- 1 | // This action counts the total number of characters in the topic column for the selected rows. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var characterCount = 0; 7 | 8 | // List all visible text columns 9 | var editor = document.editors[0]; 10 | var filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | var filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | // CREATE FORM FOR GATHERING USER INPUT 32 | var inputForm = new Form(); 33 | 34 | if (filteredColumns.includes(document.outline.outlineColumn)) { 35 | var defaultColumn = document.outline.outlineColumn; 36 | } else { 37 | var defaultColumn = document.outline.noteColumn; 38 | } 39 | 40 | var columnField = new Form.Field.Option( 41 | "columnInput", 42 | "Column", 43 | filteredColumns, 44 | filteredColumnTitles, 45 | defaultColumn 46 | ); 47 | 48 | // ADD THE FIELDS TO THE FORM 49 | inputForm.addField(columnField); 50 | // PRESENT THE FORM TO THE USER 51 | formPrompt = "Select Column"; 52 | formPromise = inputForm.show(formPrompt, "Continue"); 53 | 54 | // VALIDATE THE USER INPUT 55 | inputForm.validate = function (formObject) { 56 | return null; 57 | }; 58 | 59 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 60 | formPromise.then(function (formObject) { 61 | var selectedColumn = formObject.values["columnInput"]; 62 | 63 | selection.items.forEach(function (item) { 64 | var textObj = item.valueForColumn(selectedColumn); 65 | if (textObj) { 66 | characterCount = characterCount + textObj.characters.length; 67 | } 68 | }); 69 | alert = new Alert("Character Count", characterCount.toString()); 70 | alert.show(); 71 | }); 72 | 73 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 74 | formPromise.catch(function (err) { 75 | console.log("form cancelled", err.message); 76 | }); 77 | }); 78 | 79 | action.validate = function (selection, sender) { 80 | // validation code 81 | // selection options: columns, document, editor, items, nodes, styles 82 | if (selection.items.length > 0) { 83 | return true; 84 | } else { 85 | return false; 86 | } 87 | }; 88 | 89 | return action; 90 | })(); 91 | _; 92 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/columnStatistics.js: -------------------------------------------------------------------------------- 1 | // This action counts the total number of words in the topic column for the selected rows. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | 7 | // List all visible decimal columns 8 | var editor = document.editors[0]; 9 | var filteredColumns = columns.filter(function (column) { 10 | if (editor.visibilityOfColumn(column)) { 11 | if ( 12 | column.type === Column.Type.Number || 13 | column.type === Column.Type.Duration 14 | ) { 15 | return column; 16 | } 17 | } 18 | }); 19 | 20 | if (filteredColumns.length === 0) { 21 | throw new Error( 22 | "This document has no number nor duration columns." 23 | ); 24 | } 25 | 26 | var filteredColumnTitles = filteredColumns.map( 27 | (column) => column.title 28 | ); 29 | 30 | // CREATE FORM FOR GATHERING USER INPUT 31 | var inputForm = new Form(); 32 | 33 | var columnField = new Form.Field.Option( 34 | "columnInput", 35 | "Column", 36 | filteredColumns, 37 | filteredColumnTitles, 38 | filteredColumns[0] 39 | ); 40 | 41 | // ADD THE FIELDS TO THE FORM 42 | inputForm.addField(columnField); 43 | // PRESENT THE FORM TO THE USER 44 | formPrompt = "Select Column"; 45 | formPromise = inputForm.show(formPrompt, "Continue"); 46 | 47 | // VALIDATE THE USER INPUT 48 | inputForm.validate = function (formObject) { 49 | return null; 50 | }; 51 | 52 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 53 | formPromise.then(function (formObject) { 54 | var selectedColumn = formObject.values["columnInput"]; 55 | 56 | var values = selection.items.map((item) => { 57 | var value = item.valueForColumn(selectedColumn); 58 | return value; 59 | }); 60 | 61 | // Remove null and undefined 62 | var values = values.filter((value) => { 63 | return value; 64 | }); 65 | 66 | values.sort(function (a, b) { 67 | return a.compare(b); 68 | }); 69 | 70 | console.log( 71 | "sorted values: ", 72 | values.map((dec) => dec.toString()) 73 | ); 74 | var sum = Decimal.zero; 75 | var size = Decimal.zero; 76 | 77 | values.forEach((value) => { 78 | sum = sum.add(value); 79 | size = size.add(Decimal.one); 80 | }); 81 | 82 | var mean = sum.divide(size); 83 | var min = values[0]; 84 | var max = values[values.length - 1]; 85 | 86 | if (values.length % 2 === 0) { 87 | var median = values[values.length / 2 - 1] 88 | .add(values[values.length / 2]) 89 | .divide(Decimal.one.add(Decimal.one)); 90 | } else { 91 | var median = values[Math.floor(values.length / 2)]; 92 | } 93 | 94 | // Compute standard deviation 95 | var squaredSum = Decimal.zero; 96 | values.forEach((value) => { 97 | squaredSum = squaredSum.add( 98 | value.subtract(mean).multiply(value.subtract(mean)) 99 | ); 100 | }); 101 | var squaredSumMean = squaredSum.divide(size); 102 | var standardDeviation = Decimal.fromString( 103 | Math.sqrt(parseFloat(squaredSumMean.toString())).toString() 104 | ); 105 | 106 | var message = "Size\n" + decimalToFloat(size) + "\n\n"; 107 | message += "Sum\n" + decimalToFloat(sum) + "\n\n"; 108 | message += "Mean\n" + decimalToFloat(mean) + "\n\n"; 109 | message += 110 | "Standard Deviation\n" + 111 | decimalToFloat(standardDeviation) + 112 | "\n\n"; 113 | message += "Maximum\n" + decimalToFloat(max) + "\n\n"; 114 | message += "Minimum\n" + decimalToFloat(min) + "\n\n"; 115 | message += "Median\n" + decimalToFloat(median); 116 | 117 | alert = new Alert("Statistics", message); 118 | alert.show(); 119 | }); 120 | 121 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 122 | formPromise.catch(function (err) { 123 | console.log("form cancelled", err.message); 124 | }); 125 | }); 126 | 127 | action.validate = function (selection, sender) { 128 | // validation code 129 | // selection options: columns, document, editor, items, nodes, styles 130 | if (selection.items.length > 0) { 131 | return true; 132 | } else { 133 | return false; 134 | } 135 | }; 136 | 137 | return action; 138 | })(); 139 | _; 140 | 141 | function decimalToFloat(decimal) { 142 | var str = decimal.toString(); 143 | var float = parseFloat(str); 144 | return parseFloat(float.toPrecision(10)); 145 | } 146 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/characterCount.strings: -------------------------------------------------------------------------------- 1 | "label" = "Character Count"; 2 | "shortLabel" = "Character Count"; 3 | "mediumLabel" = "Character Count"; 4 | "LongLabel" = "Show the total character count in selected rows"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/columnStatistics.strings: -------------------------------------------------------------------------------- 1 | "label" = "Column Statistics"; 2 | "shortLabel" = "Statistics"; 3 | "mediumLabel" = "Column Statistics"; 4 | "LongLabel" = "Show basic column statistics"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/focusSelection.strings: -------------------------------------------------------------------------------- 1 | "label" = "Focus"; 2 | "shortLabel" = "Focus"; 3 | "mediumLabel" = "Focus"; 4 | "LongLabel" = "Focus on selected rows"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/hideColumn.strings: -------------------------------------------------------------------------------- 1 | "label" = "Hide Column"; 2 | "shortLabel" = "Hide Column"; 3 | "mediumLabel" = "Hide Column"; 4 | "LongLabel" = "Hide all columns possible"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/manifest.strings: -------------------------------------------------------------------------------- 1 | "com.taxyovio.view" = "⓷ View"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/texToPNG.strings: -------------------------------------------------------------------------------- 1 | "label" = "Render LaTeX"; 2 | "shortLabel" = "LaTeX"; 3 | "mediumLabel" = "Render LaTeX"; 4 | "LongLabel" = "Render LaTeX formula"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/en.lproj/wordCount.strings: -------------------------------------------------------------------------------- 1 | "label" = "Word Count"; 2 | "shortLabel" = "Word Count"; 3 | "mediumLabel" = "Word Count"; 4 | "LongLabel" = "Show the total word count in selected rows"; -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/focusSelection.js: -------------------------------------------------------------------------------- 1 | // This action focuses the editor on the selected items. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | var selectedItems = selection.items; 7 | var editor = selection.editor; 8 | var nodes = editor.nodesForObjects(selection.items); 9 | editor.focusedItems = selectedItems; 10 | }); 11 | 12 | action.validate = function (selection, sender) { 13 | // selection options: columns, document, editor, items, nodes, styles 14 | if (selection.items.length > 0) { 15 | return true; 16 | } else { 17 | return false; 18 | } 19 | }; 20 | 21 | return action; 22 | })(); 23 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/hideColumn.js: -------------------------------------------------------------------------------- 1 | // This action hides all columns except Topic and Notes. 2 | (() => { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, outline, styles 6 | 7 | // This needs to be ran twice, otherwise the UI doesn't reflect the changes. 8 | var counter = 0; 9 | Timer.repeating(0.1, function (timer) { 10 | if (counter === 2) { 11 | timer.cancel(); 12 | } else { 13 | counter += 1; 14 | console.log(counter); 15 | hideAllColumns(); 16 | } 17 | }); 18 | }); 19 | 20 | action.validate = function (selection, sender) { 21 | // selection options: columns, document, editor, items, nodes, styles 22 | if (typeof columns !== "undefined") { 23 | var visibleColumns = columns.filter((col) => { 24 | var visible = document.editors[0].visibilityOfColumn(col); 25 | if (visible && col !== outlineColumn) { 26 | return col; 27 | } 28 | }); 29 | if (visibleColumns.length > 0) { 30 | return true; 31 | } else { 32 | return false; 33 | } 34 | } else { 35 | return false; 36 | } 37 | }; 38 | 39 | return action; 40 | })(); 41 | 42 | function hideAllColumns() { 43 | var hidableColumns = columns.filter((col) => { 44 | return col !== outlineColumn; 45 | }); 46 | hidableColumns.forEach((col) => { 47 | document.editors[0].setVisibilityOfColumn(col, false); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taxyovio/OmniOutliner-Plug-Ins/bca08f8ae5409fcac616217c604b5cfcc5458dd9/view.omnioutlinerjs/Resources/icon.png -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/texToPNG.js: -------------------------------------------------------------------------------- 1 | // This action generates and shares a base 64 encoded url of a html file to render LaTeX maths formula using mathjax 3. 2 | // The url can be opened in Safari either manually, or automated using Shortcuts, or Scriptable. 3 | (() => { 4 | var action = new PlugIn.Action(function (selection, sender) { 5 | // action code 6 | // selection options: columns, document, editor, items, nodes, outline, styles 7 | var inputForm = new Form(); 8 | 9 | var defaultText = ""; 10 | if (Pasteboard.general.hasStrings) { 11 | var defaultText = Pasteboard.general.string; 12 | } 13 | // CREATE TEXT FIELD 14 | var textField = new Form.Field.String( 15 | "textInput", 16 | "Formula", 17 | defaultText 18 | ); 19 | 20 | // CREATE TEXT FIELD 21 | var scaleField = new Form.Field.String("scaleInput", "Scale", "1.0"); 22 | 23 | inputForm.addField(textField); 24 | inputForm.addField(scaleField); 25 | formPrompt = "Render LaTeX Formula"; 26 | formPromise = inputForm.show(formPrompt, "Continue"); 27 | 28 | // VALIDATE THE USER INPUT 29 | inputForm.validate = function (formObject) { 30 | var textValue = formObject.values["textInput"]; 31 | var textStatus = textValue && textValue.length > 0 ? true : false; 32 | var scaleValue = formObject.values["scaleInput"]; 33 | var scaleStatus = 34 | scaleValue && scaleValue.length > 0 && parseFloat(scaleValue) 35 | ? true 36 | : false; 37 | return textStatus && scaleStatus; 38 | }; 39 | 40 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 41 | formPromise.then(function (formObject) { 42 | var tex = formObject.values["textInput"]; 43 | var scale = parseFloat(formObject.values["scaleInput"]); 44 | if (typeof document !== "undefined") { 45 | var height = Math.round( 46 | scale * 47 | parseFloat( 48 | document.outline.baseStyle 49 | .get(Style.Attribute.FontSize) 50 | .toString() 51 | ) 52 | ); 53 | } else { 54 | var height = 12; 55 | } 56 | console.log("input", tex, "\nfont size", height); 57 | var html = Data.fromBase64(topHTML64).toString(); 58 | html += tex; 59 | html += Data.fromBase64(midHTML64).toString(); 60 | html += height; 61 | html += Data.fromBase64(botHTML64).toString(); 62 | var urlStr = 63 | "data:text/html;base64," + Data.fromString(html).toBase64(); 64 | console.log(urlStr); 65 | var url = URL.fromString(urlStr); 66 | var sp = new SharePanel([url]); 67 | sp.show(); 68 | }); 69 | }); 70 | 71 | action.validate = function (selection, sender) { 72 | // selection options: columns, document, editor, items, nodes, styles 73 | return true; 74 | }; 75 | 76 | return action; 77 | })(); 78 | 79 | const topHTML64 = 80 | "PCFET0NUWVBFIGh0bWw+CjxoZWFkPgoJPG1ldGEgY2hhcnNldD0idXRmLTgiPgoJPG1ldGEgbmFtZT0idmlld3BvcnQiIGNvbnRlbnQ9IndpZHRoPWRldmljZS13aWR0aCI+Cgk8c2NyaXB0IGlkPSJNYXRoSmF4LXNjcmlwdCIgYXN5bmMgc3JjPSJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvbnBtL21hdGhqYXhAMy9lczUvdGV4LXN2Zy1mdWxsLmpzIj48L3NjcmlwdD4KCTxzY3JpcHQ+CgkJZnVuY3Rpb24gY29udmVydCgpIHsKCQkJdmFyIGlucHV0ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImlucHV0IikudmFsdWUudHJpbSgpCgkJCXZhciBoZWlnaHQgPSBNYXRoLnJvdW5kKHBhcnNlRmxvYXQoZG9jdW1lbnQuZ2V0RWxlbWV" + 81 | "udEJ5SWQoImhlaWdodCIpLnZhbHVlLnRyaW0oKSkpCgkJCXZhciBkaXNwbGF5ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImRpc3BsYXkiKQoJCQl2YXIgYnV0dG9uID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInJlbmRlciIpCgkJCWJ1dHRvbi5kaXNhYmxlZCA9IGRpc3BsYXkuZGlzYWJsZWQgPSB0cnVlCgkJCW91dHB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdvdXRwdXQnKQoJCQlzdmcgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnc3ZnJykKCQkJcG5nID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3BuZycpCgkJCW91dHB1dC5pbm5lckhUTUwgPSAnJwoJCQlzdmcuaW5uZXJIVE1MID0gJycKCQkJcG5nLmlubmVySFRNTC" + 82 | "A9ICcnCgkJCU1hdGhKYXgudGV4UmVzZXQoKQoJCQl2YXIgb3B0aW9ucyA9IE1hdGhKYXguZ2V0TWV0cmljc0ZvcihvdXRwdXQpCgkJCW9wdGlvbnMuZGlzcGxheSA9IGRpc3BsYXkuY2hlY2tlZAoJCQlNYXRoSmF4LnRleDJzdmdQcm9taXNlKGlucHV0LCBvcHRpb25zKS50aGVuKGZ1bmN0aW9uIChub2RlKSB7CgkJCQlvdXRwdXQuYXBwZW5kQ2hpbGQobm9kZSkKCQkJCWxldCBtak91dCA9IG5vZGUuZ2V0RWxlbWVudHNCeVRhZ05hbWUoInN2ZyIpWzBdCgkJCQlvdXRwdXQuc3ZnID0gbWpPdXQub3V0ZXJIVE1MCgkJCQl2YXIgaW1hZ2UgPSBuZXcgSW1hZ2UoKQoJCQkJaW1hZ2Uuc3JjID0gJ2RhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsJyArIHdpb" + 83 | "mRvdy5idG9hKHVuZXNjYXBlKGVuY29kZVVSSUNvbXBvbmVudChvdXRwdXQuc3ZnKSkpCgkJCQlpbWFnZS5vbmxvYWQgPSBmdW5jdGlvbigpIHsKCQkJCQl2YXIgY2FudmFzID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnY2FudmFzJykKCQkJCQljYW52YXMud2lkdGggPSBpbWFnZS53aWR0aAoJCQkJCWNhbnZhcy5oZWlnaHQgPSBpbWFnZS5oZWlnaHQKCQkJCQl2YXIgY29udGV4dCA9IGNhbnZhcy5nZXRDb250ZXh0KCcyZCcpCgkJCQkJY29udGV4dC5kcmF3SW1hZ2UoaW1hZ2UsIDAsIDApCgkJCQkJaW1hZ2UuaGVpZ2h0ID0gTWF0aC5yb3VuZChpbWFnZS5oZWlnaHQgKiBoZWlnaHQgLyAxMi4gKSAvLyBkZWZhdWx0IDEyIAoJCQkJCXN2Zy5hcHBl" + 84 | "bmRDaGlsZChpbWFnZSkKCQkJCQljb25zb2xlLmxvZyhpbWFnZS5zcmMpCgkJCQkJY2FudmFzLnRvQmxvYihmdW5jdGlvbihibG9iKSB7CgkJCQkJCXZhciBuZXdJbWcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdpbWcnKQoJCQkJCQl2YXIgdXJsID0gVVJMLmNyZWF0ZU9iamVjdFVSTChibG9iKQoJCQkJCQluZXdJbWcuaGVpZ2h0ID0gaW1hZ2UuaGVpZ2h0CgkJCQkJCW5ld0ltZy5zcmMgPSB1cmwKCQkJCQkJcG5nLmFwcGVuZENoaWxkKG5ld0ltZykKCQkJCQkJY29uc29sZS5sb2coY2FudmFzLnRvRGF0YVVSTCgpKQoJCQkJCQluZXdJbWcub25sb2FkID0gZnVuY3Rpb24oKSB7CgkJCQkJCQlVUkwucmV2b2tlT2JqZWN0VVJMKHVybCkKCQkJCQk" + 85 | "JfQoJCQkJCQkvL2xvY2F0aW9uLnJlcGxhY2UodXJsKQoJCQkJCX0pCgkJCQl9CgkJCQlpbWFnZS5vbmVycm9yID0gZnVuY3Rpb24oKSB7CgkJCQkJcmVqZWN0KCkKCQkJCX0KCQkJCU1hdGhKYXguc3RhcnR1cC5kb2N1bWVudC5jbGVhcigpCgkJCQlNYXRoSmF4LnN0YXJ0dXAuZG9jdW1lbnQudXBkYXRlRG9jdW1lbnQoKQoJCQl9KS5jYXRjaChmdW5jdGlvbiAoZXJyKSB7CgkJCQlvdXRwdXQuYXBwZW5kQ2hpbGQoZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgncHJlJykpLmFwcGVuZENoaWxkKGRvY3VtZW50LmNyZWF0ZVRleHROb2RlKGVyci5tZXNzYWdlKSkKCQkJfSkudGhlbihmdW5jdGlvbiAoKSB7CgkJCQlidXR0b24uZGlzYWJsZWQgPSBkaXNwbG" + 86 | "F5LmRpc2FibGVkID0gZmFsc2UKCQkJfSkKCQl9Cgk8L3NjcmlwdD4KCTxzdHlsZT4KCSNmcmFtZSB7CgkJbWF4LXdpZHRoOiA0MGVtOwoJCW1hcmdpbjogYXV0bzsKCX0KCSNpbnB1dCB7CgkJYm9yZGVyOiAxcHggc29saWQgZ3JleTsKCQltYXJnaW46IDAgMCAuMjVlbTsKCQl3aWR0aDogMTAwJTsKCQlmb250LXNpemU6IDEyMCU7CgkJYm94LXNpemluZzogYm9yZGVyLWJveDsKCX0KCSNvdXRwdXQsICNzdmcsICNwbmcsICN1cmwgewoJCWZvbnQtc2l6ZTogMTIwJTsKCQltYXJnaW4tdG9wOiAuNzVlbTsKCQlib3JkZXI6IDFweCBzb2xpZCBncmV5OwoJCXBhZGRpbmc6IC4yNWVtOwoJCW1pbi1oZWlnaHQ6IDJlbTsKCX0KCSNvdXRwdXQgPiBwcmUge" + 87 | "woJCW1hcmdpbi1sZWZ0OiA1cHg7Cgl9CgkubGVmdCB7CgkJZmxvYXQ6IGxlZnQ7Cgl9CgkucmlnaHQgewoJCWZsb2F0OiByaWdodDsKCX0KCS5jZW50ZXIgewoJCXRleHQtYWxpZ246IGNlbnRlcjsKCX0KCTwvc3R5bGU+CjwvaGVhZD4KPGJvZHk+CjxkaXYgaWQ9ImZyYW1lIj4KPHRleHRhcmVhIGlkPSJpbnB1dCIgcm93cz0iMTYiIGNvbHM9IjEwIj4K"; 88 | 89 | const midHTML64 = 90 | "CjwvdGV4dGFyZWE+CjxiciAvPgo8ZGl2IGNsYXNzPSJsZWZ0Ij4KPGlucHV0IHR5cGU9ImNoZWNrYm94IiBpZD0iZGlzcGxheSIgY2hlY2tlZD4gPGxhYmVsIGZvcj0iZGlzcGxheSI+RGlzcGxheSBNb2RlPC9sYWJlbD4KPC9kaXY+CjxkaXYgY2xhc3M9InJpZ2h0Ij4KPGlucHV0IHR5cGU9ImJ1dHRvbiIgdmFsdWU9IlJlbmRlciBUZVgiIGlkPSJyZW5kZXIiIG9uY2xpY2s9ImNvbnZlcnQoKSIgLz4KPC9kaXY+CjxiciBjbGVhcj0iYWxsIiAvPgo8ZGl2IGNsYXNzPSJjZW50ZXIiIGlkPSJvdXRwdXQiIHN0eWxlPSJkaXNwbGF5OiBub25lOyI+b3V0cHV0PC9kaXY+CjxkaXYgY2xhc3M9ImNlbnRlciIgaWQ9InN2ZyI+c3ZnPC9kaXY" + 91 | "+CjxkaXYgY2xhc3M9ImNlbnRlciIgaWQ9InBuZyI+cG5nPC9kaXY+CjwvZGl2Pgo8dGV4dGFyZWEgaWQ9ImhlaWdodCIgc3R5bGU9ImRpc3BsYXk6IG5vbmU7Ij4K"; 92 | 93 | const height64 = "MjQ="; 94 | 95 | const botHTML64 = "CjwvdGV4dGFyZWE+CjwvYm9keT4KPC9odG1sPg=="; 96 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/Resources/wordCount.js: -------------------------------------------------------------------------------- 1 | // This action counts the total number of words in the topic column for the selected rows. 2 | var _ = (function () { 3 | var action = new PlugIn.Action(function (selection, sender) { 4 | // action code 5 | // selection options: columns, document, editor, items, nodes, styles 6 | var wordcount = 0; 7 | 8 | // List all visible text columns 9 | var editor = document.editors[0]; 10 | var filteredColumns = columns.filter(function (column) { 11 | if (editor.visibilityOfColumn(column)) { 12 | if (column.type === Column.Type.Text) { 13 | return column; 14 | } 15 | } 16 | }); 17 | 18 | if (filteredColumns.length === 0) { 19 | throw new Error("This document has no text columns."); 20 | } 21 | 22 | var filteredColumnTitles = filteredColumns.map(function (column) { 23 | if (column.title !== "") { 24 | return column.title; 25 | } else if (column === document.outline.noteColumn) { 26 | // The note column has empty title for unknown reason 27 | return "Notes"; 28 | } 29 | }); 30 | 31 | // CREATE FORM FOR GATHERING USER INPUT 32 | var inputForm = new Form(); 33 | 34 | if (filteredColumns.includes(document.outline.outlineColumn)) { 35 | var defaultColumn = document.outline.outlineColumn; 36 | } else { 37 | var defaultColumn = document.outline.noteColumn; 38 | } 39 | 40 | var columnField = new Form.Field.Option( 41 | "columnInput", 42 | "Column", 43 | filteredColumns, 44 | filteredColumnTitles, 45 | defaultColumn 46 | ); 47 | 48 | // ADD THE FIELDS TO THE FORM 49 | inputForm.addField(columnField); 50 | // PRESENT THE FORM TO THE USER 51 | formPrompt = "Select Column"; 52 | formPromise = inputForm.show(formPrompt, "Continue"); 53 | 54 | // VALIDATE THE USER INPUT 55 | inputForm.validate = function (formObject) { 56 | return null; 57 | }; 58 | 59 | // PROCESSING USING THE DATA EXTRACTED FROM THE FORM 60 | formPromise.then(function (formObject) { 61 | var selectedColumn = formObject.values["columnInput"]; 62 | 63 | selection.items.forEach(function (item) { 64 | var textObj = item.valueForColumn(selectedColumn); 65 | if (textObj) { 66 | wordcount = wordcount + textObj.words.length; 67 | } 68 | }); 69 | alert = new Alert("Word Count", wordcount.toString()); 70 | alert.show(); 71 | }); 72 | 73 | // PROMISE FUNCTION CALLED UPON FORM CANCELLATION 74 | formPromise.catch(function (err) { 75 | console.log("form cancelled", err.message); 76 | }); 77 | }); 78 | 79 | action.validate = function (selection, sender) { 80 | // validation code 81 | // selection options: columns, document, editor, items, nodes, styles 82 | if (selection.items.length > 0) { 83 | return true; 84 | } else { 85 | return false; 86 | } 87 | }; 88 | 89 | return action; 90 | })(); 91 | _; 92 | -------------------------------------------------------------------------------- /view.omnioutlinerjs/manifest.json: -------------------------------------------------------------------------------- 1 | {"author":"Taxyovio","libraries":[{"identifier":"ApplicationLib"}],"identifier":"com.taxyovio.view","version":"2021.03.17","description":"A collection of Omni Automation scripts customising the view of data in OmniOutliner.","actions":[ 2 | {"image":"plus.magnifyingglass","identifier":"focusSelection"}, 3 | {"image":"eye.slash","identifier":"hideColumn"}, 4 | {"image":"a.circle","identifier":"wordCount"}, 5 | {"image":"asterisk.circle","identifier":"characterCount"}, 6 | {"image":"chart.pie","identifier":"columnStatistics"}, 7 | {"image":"123.rectangle","identifier":"texToPNG"}, 8 | ],"defaultLocale":"en"} --------------------------------------------------------------------------------