I'm trying to create an app script sends an email when a new row is added into my Google sheet.
I have everything working for when I manually add in the data. However, when the data is auto added based on a form fill-- the email doesn't send.
I'm thinking that if I add a script that copies & pastes the email address after the new line is created, it will trigger the script to send an email. As of right now, auto-added rows do not send emails.
Thanks for the help!
Here's a link to my sheet.
Here's the App Script I'm using:
function sendEmailNotificationAndInsertTimestamp_(e) {
try {
if (!e) {
throw new Error('Please do not run the script in the script editor window but set up a trigger. See the instructions in the script.');
}
var range = e.range ? e.range : SpreadsheetApp.getActive().getActiveSheet().getActiveRange();
if (!range || JSON.stringify(e.range) === '{}') {
return;
}
var sheet = range.getSheet();
if (range.getA1Notation() === 'A1' && sheet.getName() === SpreadsheetApp.getActive().getSheets()[0].getName()) {
return;
}
var notificationSettings = [
{
sheetsToWatch: /^(EmailSender)$/i,
columnLabelsToWatch: ['Email'],
columnValuesToWatch: [undefined],
columnLabelsOfEmailAddresses: ['Send to'],
columnLabelsOfEmailSubjects: ['Subject'],
columnLabelsOfEmailContents: ['Body'],
columnLabelsToTimestamp: ['Completed Timestamp'],
insertTimestamps: [true],
overwriteTimestamps: [true],
eraseTimestamps: [false],
timestampFormat: 'm/d/yyyy h:mm am/pm',
columnLabelRow: sheet.getFrozenRows() || 1,
}
];
var rowStart = range.getRow();
var columnStart = range.getColumn();
var settings = getSettings_(sheet, notificationSettings);
if (!settings || rowStart <= settings.columnLabelRow) {
return;
}
var keysToRead = ['columnLabelsToWatch', 'columnLabelsOfEmailAddresses', 'columnLabelsOfEmailSubjects', 'columnLabelsOfEmailContents', 'columnLabelsToTimestamp'];
var columnLabelArrayLengths = ['columnValuesToWatch'].concat(keysToRead).map(getArrayLengths_(settings));
if (!isStrictlyEqual_.apply(null, columnLabelArrayLengths)) {
throw new Error('Mismatch between number of items in ' + keysToRead.join(', ') + '.');
}
var keysToWrite = ['columnsToWatch', 'columnsOfEmailAddresses', 'columnsOfEmailSubjects', 'columnsOfEmailContents', 'columnsToTimestamp'];
if (keysToRead.length !== keysToWrite.length) {
throw new Error('The number of items in keysToRead differs from the number of items in keysToWrite.');
}
var columnNumbers = getColumnNumbers_(sheet, settings, keysToRead, keysToWrite);
if (!columnNumbers) {
return;
}
var columnNumberArrayLengths = keysToWrite.map(getArrayLengths_(columnNumbers));
if (!isStrictlyEqual_.apply(null, columnNumberArrayLengths)) {
throw new Error('Could not find all the columns in ' + keysToRead.join(', ') + '.');
}
// iterate modified values
var now = new Date();
var emailAddressCheck = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
var values = range.getDisplayValues();
var emailRecipients = [];
for (var column = 0, numColumns = values[0].length; column < numColumns; column++) {
var columnIndex = columnNumbers.columnsToWatch.indexOf(columnStart + column);
if (columnIndex === -1) {
continue;
}
var toWatch = settings.columnValuesToWatch[columnIndex];
var regex
= toWatch === undefined
? /./i
: typeof toWatch === 'string'
? new RegExp('^' + escapeRegExCharacters_(toWatch) + '$', 'i')
: toWatch instanceof RegExp
? toWatch
: null;
if (!regex) {
throw new Error('All columnValuesToWatch must be regular expressions or text strings, but "' + toWatch + '" is a ' + (typeof toWatch) + '.');
}
for (var row = 0, numRows = values.length; row < numRows; row++) {
var value = values[row][column];
if (value === '' && settings.columnValuesToWatch[columnIndex] === undefined) {
continue;
}
if (!value.match(regex)) {
continue;
}
var emailAddress = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailAddresses[columnIndex]).getDisplayValue().trim();
var emailSubject = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailSubjects[columnIndex]).getDisplayValue().trim();
var emailContents = sheet.getRange(rowStart + row, columnNumbers.columnsOfEmailContents[columnIndex]).getDisplayValue().trim();
if (emailAddress.split(',').some(address => !address.trim().replace(/^(.+<)|(>$)/g, '').match(emailAddressCheck))
|| [emailAddress, emailSubject, emailContents].some(string => string === '')) {
throw new Error('Cannot send email with these parameters: To: "' + emailAddress + '", Subject: "' + emailSubject + '", Content: "' + emailContents + '"');
}
// send email
MailApp.sendEmail(emailAddress, emailSubject, emailContents);
emailRecipients.push(emailAddress);
// insert timestamp
if (settings.insertTimestamps[columnIndex]) {
var timestampCell = sheet.getRange(rowStart + row, columnNumbers.columnsToTimestamp[columnIndex]);
var timestamp
= value.length
? now
: settings.eraseTimestamps[columnIndex]
? null
: now;
if (timestamp && !settings.overwriteTimestamps[columnIndex] && timestampCell.getValue()) {
continue;
}
timestampCell.setValue(timestamp).setNumberFormat(settings.timestampFormat);
}
} // row
} // column
if (emailRecipients.length) {
showMessage_('Sent email of the status change to ' + emailRecipients.filter(filterUniques_).join(', ') + '.');
}
} catch (error) {
showAndThrow_(error);
}
}
function getSettings_(sheet, settingsArray) {
// version 1.0, written by --Hyde, 26 February 2020
// - initial version
var settings = null;
var sheetName = sheet.getName();
for (var s = 0, numSettings = settingsArray.length; s < numSettings; s++) {
if (settingsArray[s].sheetsToWatch.test(sheetName)) {
settings = settingsArray[s];
break;
}
}
return settings;
}
function getColumnNumbers_(sheet, settings, keysToRead, keysToWrite) {
// version 1.0, written by --Hyde, 26 February 2020
// - initial version
var columnLabelRowValues = sheet.getRange(settings.columnLabelRow, /*column*/ 1, /*numRows*/ 1, sheet.getLastColumn()).getValues()[0];
var columnLabels = columnLabelRowValues.map(function stringTrim(value) {
return String(value).trim();
});
var arrays = {};
for (var k = 0, numKeys = keysToRead.length; k < numKeys; k++) {
var keyToRead = keysToRead[k];
var keyToWrite = keysToWrite[k];
arrays[keyToWrite] = [];
for (var i = 0, col, numColumns = settings[keyToRead].length; i < numColumns; i++) {
if ((col = columnLabels.indexOf(settings[keyToRead][i])) !== -1) {
arrays[keyToWrite].push(col + 1);
}
}
}
return arrays[keysToWrite[0]].length ? arrays : null;
}
function getArrayLengths_(obj) {
// version 1.0, written by --Hyde 28 February 2020
// - initial version
var closure = function(key) {
return obj[key].length;
};
return closure;
}
function isStrictlyEqual_(value1, value2) {
// version 1.0, written by --Hyde 28 February 2020
// - initial version
var args = Array.prototype.slice.call(arguments);
var numArgs = args.length;
if (!numArgs) {
return null;
}
var checkAgainst = args[0];
for (var a = (1); a < numArgs; a++) {
if (args[a] !== checkAgainst) {
return false;
}
}
return true;
}
function filterUniques_(element, index, array) {
// version 1.0, written by --Hyde, 30 May 2019
// - initial version
return array.indexOf(element) === index;
}
function escapeRegExCharacters_(string) {
// version 1.1, written by --Hyde, 9 November 2018
// - use String
// version 1.0, written by --Hyde, 30 November 2018
// - initial version
// - adapted from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
return String(string).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function showAndThrow_(error) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
var stackCodeLines = String(error.stack).match(/\d+:/);
if (stackCodeLines) {
var codeLine = stackCodeLines.join(', ').slice(0, -1);
} else {
codeLine = error.stack;
}
showMessage_(error.message + ' Code line: ' + codeLine, 30);
throw error;
}
function showMessage_(message, timeoutSeconds) {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
SpreadsheetApp.getActive().toast(message, 'Send email notification script', timeoutSeconds || 5);
}
function installOnEditTrigger() {
// version 1.0, written by --Hyde, 9 January 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_EDIT);
deleteTriggers_(ScriptApp.EventType.ON_CHANGE);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function installOnFormSubmitTrigger() {
// version 1.0, written by --Hyde, 16 April 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_FORM_SUBMIT);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onFormSubmit()
.create();
}
function installOnChangeTrigger() {
// version 1.0, written by --Hyde, 7 May 2020
// - initial version
deleteTriggers_(ScriptApp.EventType.ON_EDIT);
deleteTriggers_(ScriptApp.EventType.ON_CHANGE);
ScriptApp.newTrigger('sendEmailNotificationAndInsertTimestamp_')
.forSpreadsheet(SpreadsheetApp.getActive())
.onChange()
.create();
}
function deleteTriggers_(triggerType) {
// version 1.0, written by --Hyde, 7 May 2020
// - initial version
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0, numTriggers = triggers.length; i < numTriggers; i++) {
if (triggers[i].getEventType() === triggerType) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
}
Related
I'm creating a Gmail script that includes 5 variables, one of which is a due date. I just want it to populate as MM/DD/YYYY, however, it is currently populating as Thu Sep 13 2018 00:00:00 GMT-0400 (EDT).
Is there a way I can do that? I've pasted my code below for your reference. Any assistance is much appreciated.
function getRowsData(sheet, range, columnHeadersRowIndex) {
columnHeadersRowIndex = columnHeadersRowIndex || range.getRowIndex() - 1;
var numColumns = range.getEndColumn() - range.getColumn() + 1;
var headersRange = sheet.getRange(columnHeadersRowIndex, range.getColumn(), 1, numColumns);
var headers = headersRange.getValues()[0];
return getObjects(range.getValues(), normalizeHeaders(headers));
}
function getObjects(data, keys) {
var objects = [];
for (var i = 0; i < data.length; ++i) {
var object = {};
var hasData = false;
for (var j = 0; j < data[i].length; ++j) {
var cellData = data[i][j];
if (isCellEmpty(cellData)) {
continue;
}
object[keys[j]] = cellData;
hasData = true;
}
if (hasData) {
objects.push(object);
}
}
return objects;
}
function normalizeHeaders(headers) {
var keys = [];
for (var i = 0; i < headers.length; ++i) {
var key = normalizeHeader(headers[i]);
if (key.length > 0) {
keys.push(key);
}
}
return keys;
}
function normalizeHeader(header) {
var key = "";
var upperCase = false;
for (var i = 0; i < header.length; ++i) {
var letter = header[i];
if (letter == " " && key.length > 0) {
upperCase = true;
continue;
}
if (!isAlnum(letter)) {
continue;
}
if (key.length == 0 && isDigit(letter)) {
continue;
}
if (upperCase) {
upperCase = false;
key += letter.toUpperCase();
} else {
key += letter.toLowerCase();
}
}
return key;
}
function isCellEmpty(cellData) {
return typeof(cellData) == "string" && cellData == "";
}
function isAlnum(char) {
return char >= 'A' && char <= 'Z' ||
char >= 'a' && char <= 'z' ||
isDigit(char);
}
function isDigit(char) {
return char >= '0' && char <= '9';
i'm looking for a way to select items in the render queue via script.
i'm creating a listBox that contains all the RQ items ( i need to list them in a simpler way then the RQ window, and i'm keeping the index number of each item), and i want to select from that list items and select them in the RQ.
ideas?
thanks
Dror
try{
function createUserInterface (thisObj,userInterfaceString,scriptName){
var pal = (thisObj instanceof Panel) ? thisObj : new Window("palette", scriptName,
undefined,{resizeable: true});
if (pal == null) return pal;
var UI=pal.add(userInterfaceString);
pal.layout.layout(true);
pal.layout.resize();
pal.onResizing = pal.onResize = function () {
this.layout.resize();
}
if ((pal != null) && (pal instanceof Window)) {
pal.show();
}
return UI;
};
{var res ="group {orientation:'column',\
alignment:['fill','fill'],\
alignChildren:['fill','top'],\
folderPathListbox:ListBox{\
alignment:['fill','fill'],\
properties:{\
multiline:true,\
multiselect:true,\
numberOfColumns:6,\
showHeaders:true,\
columnTitles: ['#','OM','Date','Time', 'Comp Name', 'Render Path']}\
},\
buttonGroup:Group{orientation:'row',\
alignment:['fill','bottom']\
alignChildren:['fill','bottom'],\
buttonPanel: Panel{\
text:'Actions',\
orientation: 'row',\
alignment:['fill','bottom']\
refButton: Button{text:'Refresh'}\
dupButton: Button{text:'Duplicate'}\
selButton: Button{text:'Select in RQ'}\
expButton: Button{text:'Export'}\
}\
searchPanel: Panel{\
text:'Search',\
orientation: 'row',\
searchBox: EditText{text:'search by fileName'},\
searchButton: Button{text:'Search'}, \
}\
}\
}";
}
function listRQ (rqList){
try{
var folderPathListbox = rqList;
var proj = app.project;
var totalRenderQ = proj.renderQueue.numItems;
for(var i= 1; i<=totalRenderQ; i++){
var totalOM= proj.renderQueue.item(i).numOutputModules;
for (var om= 1; om<=totalOM; om++){
var dateList, timeList, curItem;
if (proj.renderQueue.item(i).startTime != null){
var min = proj.renderQueue.item(i).startTime.getMinutes() <10 ? "0"+ proj.renderQueue.item(i).startTime.getMinutes() : proj.renderQueue.item(i).startTime.getMinutes();
var year = proj.renderQueue.item(i).startTime.getFullYear().toString().substr (-2,2);
timeList = (proj.renderQueue.item(i).startTime.getHours()-1)+":" + min;
dateList =proj.renderQueue.item(i).startTime.getDate()+"/"+(proj.renderQueue.item(i).startTime.getMonth()+1)+"/"+year ;
}else{
dateList = "not ";
timeList = "rendered";
}
curItem = folderPathListbox.add ('item', i ); // Column 1
curItem.subItems[0].text = om; // Column 2
curItem.subItems[1].text = dateList.toString(); // Column 3
curItem.subItems[2].text = timeList.toString(); // Column 4
curItem.subItems[3].text = proj.renderQueue.item(i).comp.name; // Column 5
curItem.subItems[4].text = proj.renderQueue.item(i).outputModule(om).file.toString().replace(new RegExp(",","g"), "\r").replace(new RegExp("%20","g"), " ").replace(new RegExp("%5B","g"), "[").replace(new RegExp("%5D","g"), "]"); // Column 6
}
}
}catch(err){alert(err)}
return folderPathListbox;
}
var UI = createUserInterface(this,res,"Better RQ");
var myList = UI.folderPathListbox;
var lsRq = listRQ(myList);
//~ alert(lsRq.toString());
{ // buttons action
UI.buttonGroup.buttonPanel.refButton.onClick = function () {
lsRq.removeAll();
listRQ(myList);
writeLn("all done");
}
UI.buttonGroup.buttonPanel.dupButton.onClick = function () {
var lstSlct = new Array ;
lstSlct = myList.selection;
if ( lstSlct != null){
var totalDup = lstSlct.length;
for (var i= 0; i<totalDup; i++){
var lsId = lstSlct[i].toString();
var dup = parseInt(lsId);
app.project.renderQueue.item(dup).duplicate();
writeLn("duplicated #"+dup);
}
}else{
alert ("select Something");
}
}
UI.buttonGroup.buttonPanel.selButton.onClick = function () {
app.project.renderQueue.showWindow(true) ; //shows the RQ
alert ("selButton");
}
UI.buttonGroup.buttonPanel.expButton.onClick = function () {
// var compName = myList.
alert ("expButton");
}
}
}
catch(err){
alert ("Error at line # " + err.line.toString() + "\r" + err.toString());
}
Below code is a copy with minor edits from https://github.com/GoogleChrome/chrome-app-samples/tree/master/serial/ledtoggle. I am able to send a byte and receive a reply. I am not able to get an TimeoutError event in case of reply is not sent by the client. I have set timeout to 50 ms.
this.receiveTimeout = 50;
Entire code follows.
const DEVICE_PATH = 'COM1';
const serial = chrome.serial;
var ab2str = function(buf) {
var bufView = new Uint8Array(buf);
var encodedString = String.fromCharCode.apply(null, bufView);
return decodeURIComponent(escape(encodedString));
};
var str2ab = function(str) {
var encodedString = unescape((str));
var bytes = new Uint8Array(1);
bytes[0] = parseInt(encodedString);
}
return bytes.buffer;
};
var SerialConnection = function() {
this.connectionId = -1;
this.lineBuffer = "";
this.receiveTimeout =50;
this.boundOnReceive = this.onReceive.bind(this);
this.boundOnReceiveError = this.onReceiveError.bind(this);
this.onConnect = new chrome.Event();
this.onReadLine = new chrome.Event();
this.onError = new chrome.Event();
};
SerialConnection.prototype.onConnectComplete = function(connectionInfo) {
if (!connectionInfo) {
log("Connection failed.");
return;
}
this.connectionId = connectionInfo.connectionId;
chrome.serial.onReceive.addListener(this.boundOnReceive);
chrome.serial.onReceiveError.addListener(this.boundOnReceiveError);
this.onConnect.dispatch();
};
SerialConnection.prototype.onReceive = function(receiveInfo) {
if (receiveInfo.connectionId !== this.connectionId) {
return;
}
this.lineBuffer += ab2str(receiveInfo.data);
var index;
while ((index = this.lineBuffer.indexOf('$')) >= 0) {
var line = this.lineBuffer.substr(0, index + 1);
this.onReadLine.dispatch(line);
this.lineBuffer = this.lineBuffer.substr(index + 1);
}
};
SerialConnection.prototype.onReceiveError = function(errorInfo) {
log('Error');
if (errorInfo.connectionId === this.connectionId) {
log('Error');
this.onError.dispatch(errorInfo.error);
log('Error');
}
log('Error');
};
SerialConnection.prototype.connect = function(path) {
serial.connect(path, this.onConnectComplete.bind(this))
};
SerialConnection.prototype.send = function(msg) {
if (this.connectionId < 0) {
throw 'Invalid connection';
}
serial.send(this.connectionId, str2ab(msg), function() {});
};
SerialConnection.prototype.disconnect = function() {
if (this.connectionId < 0) {
throw 'Invalid connection';
}
serial.disconnect(this.connectionId, function() {});
};
var connection = new SerialConnection();
connection.onConnect.addListener(function() {
log('connected to: ' + DEVICE_PATH);
);
connection.onReadLine.addListener(function(line) {
log('read line: ' + line);
});
connection.onError.addListener(function() {
log('Error: ');
});
connection.connect(DEVICE_PATH);
function log(msg) {
var buffer = document.querySelector('#buffer');
buffer.innerHTML += msg + '<br/>';
}
document.querySelector('button').addEventListener('click', function() {
connection.send(2);
});
Maybe I'm reading the code incorrectly, but at no point do you pass receiveTimeout into chrome.serial. The method signature is chrome.serial.connect(string path, ConnectionOptions options, function callback), where options is an optional parameter. You never pass anything into options. Fix that and let us know what happens.
I want to run a function that updates some values when I edit one cell of a column. This line of the trigger works well: dataCell0.setValue(today_date(new Date())[2]);. But this other line updatePercent(); doesn't. But if I call this updatePercent() function from a time based trigger (in Resources), it works well. What is going wrong with this updatePercent() call?
function onEdit(){
var s = SpreadsheetApp.getActiveSheet();
if( ( s.getName() == "mySheet1" ) || (s.getName() == "mySheet2") ) { //checks that we're on the correct sheet
var r = s.getActiveCell();
if( s.getRange(1, r.getColumn()).getValue() == "PORCENT_TIME") { // If you type a porcent, it adds its date.
var dataCell0 = r.offset(0, 1);
dataCell0.setValue(today_date(new Date())[2]);
updatePercent();
}
}
}
Here the updatePercent function code:
/**
* A function to update percent values accoding to input date.
**/
function updatePercent() {
var sheet = SpreadsheetApp.getActiveSheet();
var column = getColumnNrByName(sheet, "PORCENT_TIME");
var input = sheet.getRange(2, column+1, sheet.getLastRow(), 4).getValues();
var output = [];
for (var i = 0; i < input.length; i++) {
var fulfilledPercent = input[i][0];
Logger.log("fulfilledPercent = " + fulfilledPercent);
var finalDate = input[i][3];
Logger.log("finalDate = " + input[i][3]);
if ( (typeof fulfilledPercent == "number") && (finalDate instanceof Date) ) {
var inputDate = input[i][1]; // Date when input was added.
var restPorcentPen = 100 - fulfilledPercent;
var restantDays = dataDiff(inputDate, finalDate);
var percentDay = restPorcentPen/restantDays;
Logger.log("percentDay = " + percentDay);
var passedTime = dataDiff(inputDate, new Date());
Logger.log("passedTime = " + passedTime);
var passedPorcent = passedTime * percentDay; // How much percent this passed time is?
Logger.log("passedPorcent = " + passedPorcent);
var newPorcent = (fulfilledPercent + passedPorcent);
newPorcent = Math.round(newPorcent * 100) / 100;
Logger.log("newPorcent = " + newPorcent);
var newInputDate = hoje_data(new Date())[2]; // Now update the new input date
// newPorcent = newPorcent.toFixed(2);
output.push([newPorcent, newInputDate]);
sheet.getRange(2, column+1, output.length, 2).setValues(output);
Logger.log(" ");
var column25Dec = getColumnNrByName(sheet, "PORCENT_25DEZ");
var passedTimeSince25Dec = dataDiff(new Date(2013,11,25), new Date()); // Months: January is 0;
var decPercent = (newPorcent - (passedTimeSince25Dec * percentDay)); // .toFixed(2).replace(".", ",");
decPercent = Math.round(decPercent * 100) / 100;
// if (sheet.getRange(output.length+1, column25Dec+1).getValues() == ''){
sheet.getRange(output.length+1, column25Dec+1).setValue(decPercent );
// }
var remainingYears = dataDiffYears(new Date(), finalDate);
sheet.getRange(output.length+1, column).setValue(remainingYears);
}
else {
newPorcent = "Put a final date"
output.push([newPorcent, inputDate]);
sheet.getRange(2, column+1, output.length, 2).setValues(output);
}
if (finalDate instanceof Date){
var remainingYears = dataDiffYears(new Date(), finalDate);
// Logger.log("remainingYears = " + remainingYears);
}
else {
remainingYears = "insert a valid date";
}
sheet.getRange(output.length+1, column).setValue(remainingYears);
}
}
I will guess you're using the new gSheets. Check if it will work in the old-style sheets. The new sheets' onEdit trigger has problems, particularly with getActive.
My problem was in the updatePercent() funciton. Thank you, guys!
I have a page where I am displaying some text in a div and I need to highlight this text in certain parts. I have done this by surrounding the text I need to highlight with a tag and appropriate css styling.
E.g.
<div>
My text will look like this with <span class="highlight">highlighted bits</span> in it.
</div>
This works fine. However, another requirement for this page is that the user must be able to select texts, click a button, and the selected text must be highlighted too.
The problem I have is when trying to identify the range of the selected text to grab (using window.getSelection.getRangeAt(0)), this gives me the range which resets after every <span> tag in the text, not from the beginning of the text.
For those who would like to know in the future this is how I did it:
jQuery.fn.highlight = function(startOffset,endOffset,type) {
function innerHighlight(node, startOffset,endOffset) {
var calledStartOffset = parseInt(startOffset);
var startOffsetNode=getChildNodeForOffset(node,parseInt(startOffset));
var endOffsetNode=getChildNodeForOffset(node,parseInt(endOffset));
startOffset = resizeOffsetForNode(startOffsetNode,parseInt(startOffset));
if (startOffsetNode == endOffsetNode){
endOffset = resizeOffsetForNode(endOffsetNode,parseInt(endOffset));
highlightSameNode(startOffsetNode, parseInt(startOffset),parseInt(endOffset),type,calledStartOffset);
} else {
highlightDifferentNode(startOffsetNode,endOffsetNode,parseInt(startOffset),parseInt(endOffset),type,calledStartOffset);
}
}
return this.each(function() {
innerHighlight(this, startOffset,endOffset);
});
};
function resizeOffsetForNode(offsetNode,offset){
if (offsetNode.id >= 0){
offset = parseInt(offset)-parseInt(offsetNode.id);
} else if (offsetNode.previousSibling != null && offsetNode.previousSibling.id > 0){
offset = parseInt(offset)-parseInt(offsetNode.previousSibling.id)-parseInt(offsetNode.previousSibling.textContent.length);
}
return offset;
}
function getChildNodeForOffset(testNode,offset) {
if (testNode.nodeType == 1 && testNode.childNodes && !/(script|style)/i.test(testNode.tagName)) {
var offsetNode=null;
var currentNode;
for (var i = 0; i < testNode.childNodes.length; ++i) {
currentNode=testNode.childNodes[i];
if (currentNode.id >= 0 && parseInt(currentNode.id) <= parseInt(offset) && ((parseInt(currentNode.id) + parseInt(currentNode.textContent.length)) >= parseInt(offset))){
offsetNode = currentNode;
break;
} else if (currentNode.id >= 0 && parseInt(currentNode.id) > parseInt(offset)){
offsetNode = currentNode.previousSibling;
break;
}
}
if (offsetNode==null){
offsetNode = testNode.childNodes[testNode.childNodes.length-1];
}
return offsetNode;
}
}
function highlightSameNode(node, startOffset,endOffset,type,calledStartOffset) {
var skip = 0;
if (node.nodeType == 3) {
if (startOffset >= 0) {
var spannode = document.createElement('span');
spannode.className = 'entity '+ type;
spannode.id=calledStartOffset;
var middlebit = node.splitText(startOffset);
var endbit = middlebit.splitText(endOffset-startOffset);
var middleclone = middlebit.cloneNode(true);
spannode.appendChild(middleclone);
middlebit.parentNode.replaceChild(spannode, middlebit);
}
} else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) {
var childnode = node.childNodes[0];
highlightSameNode(childnode, startOffset,endOffset,type,calledStartOffset);
}
}
function highlightDifferentNode(startnode, endnode, startOffset,endOffset,type,calledStartOffset) {
var skip = 0;
if (startnode.nodeName == "#text") {
if (startOffset >= 0) {
var spannode = document.createElement('span');
spannode.className = 'entity '+ type;
spannode.id=calledStartOffset;
var endbit = node.splitText(startOffset);
var endclone = endbit.cloneNode(true);
spannode.appendChild(endclone);
endbit.parentNode.replaceChild(spannode, endbit);
}
} else if (startnode.nodeName == "SPAN") {
if (startOffset >= 0) {
var spannode = document.createElement('span');
spannode.className = 'entity '+ type;
spannode.id=calledStartOffset;
var endTextbit = startnode.childNodes[0].splitText(startOffset);
spannode.appendChild(endTextbit);
startnode.parentNode.insertBefore(spannode, startnode.nextSibling);
}
}
var currentTestNode=startnode.nextSibling;
while (currentTestNode!=endnode){
if (currentTestNode.nodeName == "#text") {
var spannode = document.createElement('span');
spannode.className = 'entity '+ type;
spannode.id=parseInt(currentTestNode.previousSibling.id)+parseInt(currentTestNode.previousSibling.textContent.length);
var currentNodeClone=currentTestNode.cloneNode(true);
spannode.appendChild(currentNodeClone);
endbit.parentNode.replaceChild(spannode, currentTestNode);
} else if (currentTestNode.nodeName == "SPAN") {
currentTestNode.className = 'entity overlap';
}
currentTestNode=currentTestNode.nextSibling;
}
var previousNodeEnd = parseInt(endnode.previousSibling.id)+parseInt(endnode.previousSibling.textContent.length);
var spannode = document.createElement('span');
spannode.className = 'entity '+ type;
spannode.id=previousNodeEnd;
if (endnode.nodeName == "#text") {
if (endOffset >= 0) {
//end offset here is the original end offset from the beginning of the text, not node
var unwantedbit = endnode.splitText(parseInt(endOffset)-parseInt(previousNodeEnd));
var endclone = endnode.cloneNode(true);
spannode.appendChild(endclone);
endnode.parentNode.replaceChild(spannode, endnode);
}
} else if (endnode.nodeName == "SPAN") {
if (endOffset >= 0) {
var wantTextbit = endnode.childNodes[0].splitText(parseInt(endOffset)-parseInt(previousNodeEnd));
spannode.appendChild(wantTextbit);
wantTextbit.parentNode.parentNode.insertBefore(spannode, endnode);
}
}
if (startnode.textContent.length < 1){
startnode.parentNode.removeChild(startnode);
}
if (endnode.textContent.length < 1){
endnode.parentNode.removeChild(endnode);
}
}
jQuery.fn.removeHighlight = function() {
return this.find("span.entity").each(function() {
this.parentNode.firstChild.nodeName;
with (this.parentNode) {
replaceChild(this.firstChild, this);
normalize();
}
}).end();
};
function contains(a, b){
return a.contains ? a != b && a.contains(b) : !!(a.compareDocumentPosition(b) & 16);
}