I am trying to copy the contents of a GDoc into another GDoc. This works pretty well for all the different element types. Including a table (Enum DocumentApp.ElementType.TABLE). However, if the table contains an inline image (Enum DocumentApp.ElementType.INLINE_IMAGE) the image is not copied correctly.
Here is a link to the source GDoc example. https://docs.google.com/document/d/14kXjC0CTkEgmD7Ttv0YKL9ubfDRHbL1hCVOqOiyiEDU/edit#. Find the row in the table with the New Zealand flag. The flag is not copied correctly into the target GDoc's new table.
I am simply finding the table object in the source document (above) and using Body::insertTable(childIndex,table) to insert it into the target GDoc's Body. Most other elements in the table copy OK. Including an embedded Google Drawing. But not the inline image.
I had a similar problem and in my case, the GLYPH_TYPE of numbered lists got lost, too. But I detected that the images will be fine copied if the CellChildren are copied one by one.
So I solved my problem by copying the new table at whole and then replacing the contents of each dstTable cell one by one from the original table.
This works fine now even with nested tables, since the function calls itself, if another table within the table is detected.
And the problem with the lost ListItem attribute is solved, too by setting the attributes additionally after the insert.
Here is my well working code segment:
Firstly I detect the table and insert it in the dstBody ...
...
dstChild = srcChild.copy();
switch( dstChild.getType() ) {
case DocumentApp.ElementType.PARAGRAPH:
...
case DocumentApp.ElementType.LIST_ITEM:
...
case DocumentApp.ElementType.TABLE:
var newTable =
dstBody.insertTable( dstBody.getNumChildren()-1, dstChild );
copyTableCellByCell( dstChild, newTable );
break;
....
}
And this is the maybe recursive function that replaces each cell by first clearing it and copying the content from the original table:
function copyTableCellByCell( srcTable, dstTable ) {
var numRows = dstTable.getNumRows();
var dstRow, numCells, dstCell, actCellIndex;
for ( var actRowIndex = 0; actRowIndex < numRows; actRowIndex++ ) {
dstRow = dstTable.getRow( actRowIndex );
numCells = dstRow.getNumCells();
for ( actCellIndex = 0; actCellIndex < numCells; actCellIndex++ ) {
dstCell = dstRow.getCell( actCellIndex );
dstCell.clear();
var srcCell = srcTable.getCell( actRowIndex, actCellIndex );
var numCellChildren = srcCell.getNumChildren();
for ( var y = 0; y < numCellChildren; y++ ) {
var cellChild = srcCell.getChild( y );
var childCopy = cellChild.copy();
switch( childCopy.getType() ) {
case DocumentApp.ElementType.PARAGRAPH:
dstCell.insertParagraph( y, childCopy );
break;
case DocumentApp.ElementType.LIST_ITEM:
// that the GLYPH_TYPE doesn't get lost
var atts = childCopy.getAttributes();
var
newListItem = dstCell.insertListItem( y, childCopy );
newListItem.setAttributes( atts );
break;
case DocumentApp.ElementType.TABLE:
var newTable =
dstCell.insertTable( y, childCopy );
copyTableCellByCell( cellChild, newTable );
break;
}
}
// remove the very first empty paragraph in the cell
while ( (y = dstCell.getNumChildren()) > numCellChildren ) {
dstCell.getChild( y - 1 ).removeFromParent();
}
}
}
}
This could be fine tuned, of course.
You could just search and pick out the inline images, if you want the servers have to do less work.
Methods that you can use to copy an Inline Image can be found in Class InlineImage and there are even additional methods if an inline image was contained within a ListItem or Paragraph.
I found a none coding solution to this bug.
Insert the image as a "drawing" in the source document.
Click the table cell where images needs to be inserted.
Click "insert" on the document's menu.
Click "insert drawing".
In drawing pane add image to be inserted.
Save and close.
The result will be an image in the table that gets copied perfectly along with the type TABLE.
Related
Let's have contenteditable div. Browser itself manage undo on it.
But when additional content changes (or touching selection ranges) are made from script (in addition to user action) then it stops behave as user expected.
In other words when user hit Ctrl+Z then div content is not reverted to previous state.
See following simplified artificial example:
https://codepen.io/farin/pen/WNEMVEB
const editor = document.getElementById("editor")
editor.addEventListener("keydown", ev => {
if (ev.key === 'a') {
const sel = window.getSelection()
const range = window.getSelection().getRangeAt(0)
const node = range.startContainer;
const value = node.nodeValue
node.nodeValue = value + 'aa'
range.setStart(node, value.length + 2)
range.setEnd(node, value.length + 2)
ev.preventDefault()
}
})
All written 'a' letters are doubled.
Undo is ok as long as there is no 'a' typed.
When user typed 'a' (appended to text as double 'aa') and hits Ctrl+Z, then he expects both 'a' will be removed and cursor moves back to original position.
Instead only one 'a' is reverted on undo and second one added by script remain.
If event is also prevented by preventDefault() (which is not needed in this example, but in my real world example i can hardly avoid it) then all is worse.
Because undo reverts previous user action.
I could images that whole undo/redo stuff will be managed by script, but it means implementation of whole undo/redo logic. That's too complicated, possible fragile and with possible many glitches.
Instead I would like tell browser something like that there is atomic change which should be reverted by one user undo. Is this possible?
You can store the "revisions" in an array, then push the innerHTML of the div to it whenever you programmatically change the innerHTML of it.
Then, you can set the innerHTML of the div to the last item in the revisions array whenever the user uses the Ctrl + Z shortcut.
const previousRevisions = []
function saveState() {
previousRevisions.push(editor.innerHTML)
}
function undoEdit() {
if (previousRevisions.length > 0) {
editor.innerHTML = previousRevisions.pop();
}
}
const editor = document.getElementById("editor")
editor.addEventListener("keydown", ev => {
if (ev.key === 'a') {
saveState()
const sel = window.getSelection()
const range = window.getSelection().getRangeAt(0)
const node = range.startContainer;
const value = node.nodeValue
node.nodeValue = value + 'a'
range.setStart(node, value.length + 1)
range.setEnd(node, value.length + 1)
} else if (ev.ctrlKey && ev.key == 'z') {
undoEdit()
}
})
#editor{width:600px;min-height:250px;border:1px solid black;font-size:24px;margin:0 auto;padding:10px;font-family:monospace;word-break:break-all}
<div id="editor" contenteditable="true">type here </div>
The benefit of this solution is that it will not conflict with the browser's native Ctrl + Z shortcut behavior.
Make the parent a div (if it isn't) and make it so it adds spans inside of it every time the user taps a so the new span and set it's id to span-keyword will have aa as the value / text. Then check if the users cursor is at the beginning of it and check if there is no other text in-front of it and the user did no other action in it. If there is no text and no other actions happened do this:
document.getElementById('span-keyword').remove();
I want to scan words in a Google doc from left to right and replace the first occurrences of some keywords with a URL or a bbcode like tag wrapper around them.
I cannot use findText API because it's not simple regex finding but complex pattern matching involving lots of if else conditions involving business logic.
Here is how I want to solve this
let document = DocumentApp.getActiveDocument().getBody();
let paragraph = document.getParagraphs()[0];
let contents = paragraph.getText();
// makeAllTheNecessaryReplacemens has all the business logic to identify which keywords need to changed
let newContents = makeAllTheNecessaryReplacemens(contents);
paragraph.setText(newContents);
The problem here is that text style gets wiped out and also makeAllTheNecessaryReplacemens cannot add hyperlinks to string text.
Please suggest a way to do this.
Proposed function
/**
* This is a wrapper around the attribute functions
* this allows setting one attribute at a time
* based of a complete attribute object obtained
* from another element. This makes it far more
* reliable.
*/
const attributeKey = {
FONT_SIZE : (o,s,e,a) => o.setFontSize(s,e,a),
STRIKETHROUGH : (o,s,e,a) => o.setStrikethrough(s,e,a),
FOREGROUND_COLOR : (o,s,e,a) => o.setForegroundColor(s,e,a),
LINK_URL : (o,s,e,a) => o.setLinkUrl(s,e,a),
UNDERLINE : (o,s,e,a) => o.setUnderline(s,e,a),
BOLD : (o,s,e,a) => o.setBold(s,e,a),
ITALIC : (o,s,e,a) => o.setItalic(s,e,a),
BACKGROUND_COLOR : (o,s,e,a) => o.setBackgroundColor(s,e,a),
FONT_FAMILY : (o,s,e,a) => o.setFontFamily(s,e,a)
}
/**
* Replace textToReplace with replacementText
* Will reatain formatting and hyperlinks
*/
function replaceTextPlus(textToReplace, replacementText) {
// Initializing
let body = DocumentApp.getActiveDocument().getBody();
let searchResult = body.findText(textToReplace);
while (searchResult != null) {
// Getting info about result
let foundElement = searchResult.getElement();
let start = searchResult.getStartOffset();
let end = searchResult.getEndOffsetInclusive();
// This returns a complete attributes object
// Many attributes have null as a value
let attributes = foundElement.getAttributes(start);
// Replacing text
foundElement.deleteText(start, end);
foundElement.insertText(start, replacementText);
// Setting new end index
let newEnd = start + replacementText.length - 1
// Set attributes for new text skipping over null values
// This requires the constant defined at the top.
for (let a in attributes) {
if (attributes[a] != null) {
attributeKey[a](foundElement, start, newEnd, attributes[a]);
}
}
// Modifies the actual searchResult so that the next findText
// starts at the NEW end index.
try {
let rangeBuilder = DocumentApp.getActiveDocument().newRange();
rangeBuilder.addElement(foundElement, start, newEnd);
searchResult = rangeBuilder.getRangeElements()[0];
} catch (e){
Logger.log("End of Document")
return null
}
// searches for next result
searchResult = body.findText(textToReplace, searchResult);
}
}
Extending the findText API
This function relies on the findText API, but it adds in a few more steps.
Find the text.
Get the element containing the text.
Get the start and end indices of the text.
Get the attributes of the text (font, color, hyperlink etc)
Replace the text.
Update the end index.
Use the old attributes to update the new text.
You call it like this:
replaceTextPlus("Bing", "Google")
replaceTextPlus("occurrences", "happenings")
replaceTextPlus("text", "prefixedtext")
How to set the formatting and link attributes.
This relies on the attributes object that gets returned from getAttributes. Which looks something like this:
{
FOREGROUND_COLOR=#ff0000,
LINK_URL=null,
FONT_SIZE=null,
ITALIC=true,
STRIKETHROUGH=null,
FONT_FAMILY=null,
BOLD=null,
UNDERLINE=true,
BACKGROUND_COLOR=null
}
I tried to use setAttributes but it was very unreliable. Using this method almost always resulted in some formatting loss.
To fix this I make an object attributeKey that wraps all the different functions for setting individual attributes, so that they can be called from this loop:
for (let a in attributes) {
if (attributes[a] != null) {
attributeKey[a](foundElement, start, newEnd, attributes[a]);
}
}
This allows null values to be skipped which seems to have solved the unreliability problem. Perhaps the update buffer gets confused with many values.
Limitations
This function gets the formatting of the first character of the found word. If the same work has different formatting within itself. For example, "Hello" (Mixed normal with bold and italic), the replacement word will have the formatting of the first letter. This could potentially be fixed by identifying the word and iterating over every single letter.
References
Text class
Body class
DocumentApp
Element Interface
Attribute Enum
I have a QTableView in my project, in which several columns display data that includes a hyperlink. I use a delegate class for these to set it up so that when the cell in the column is clicked, it opens the linked page in the browser. This works great... when it's only one value being linked to one page. For example, I may have a list of search values for mysite.com where the columns have values A, B, C, etc.. If the user clicks on the cell in this column with A, it will open a hyperlink for mysite.com/A (again, this part works fine). However, I now need to add a column that may have something like "A,B", where it needs to support links to search for A AND B in the same cell depending on which they click. I've been searching around online for a while now and it seems like this probably can't be done with a delegate. I have a line in a QTextBrowser elsewhere in my code where I can do this via HTML, like this:
QString toShow;
for(int i = 0; i < searchValueList.size(); i++)
{
toShow.append("`<a href=\"www.mysite.com/" + searchValueList.at(i) + "\"`>" +
searchValueList.at(i) + "`</a`>";
}
However I can't find any way to set the cells in a QTableView to recognize HTML formatting or Rich Text, and alas I'm not even sure that's possible. Is there any way at all to do what I'm trying to accomplish?
You can create a custom QItemDelegate for the specific column in which you can display rich text. The delegate could be like :
class RichTextDelegate: public QItemDelegate
{
public:
RichTextDelegate(QObject *parent = 0);
void paint( QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index ) const;
};
RichTextDelegate::RichTextDelegate(QObject *parent):QItemDelegate(parent)
{
}
void RichTextDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if( option.state & QStyle::State_Selected )
painter->fillRect( option.rect, option.palette.highlight() );
painter->save();
QTextDocument document;
document.setTextWidth(option.rect.width());
QVariant value = index.data(Qt::DisplayRole);
if (value.isValid() && !value.isNull())
{
document.setHtml(value.toString());
painter->translate(option.rect.topLeft());
document.drawContents(painter);
}
painter->restore();
}
You should set the item delegate for the specific column :
ui->tableView->setItemDelegateForColumn(colIndex, new RichTextDelegate(ui->tableView));
Now if you set the model text for the specific column in a row to a rich text, it will be shown properly :
model->item(rowIndex, colIndex)->setText(someRichText);
I have a method which returns a List<string> which my code uses to populate a table:
string[] siteNames;
siteNames = client.GetListSiteElementNames();
foreach (string siteName in siteNames)
{
TableCell tempCell = new TableCell();
tempCell.Text = siteName;
this.sitesTableRow.Cells.Add(tempCell);
}
This inserts the cells on to a single row, so the cells are presented horizontally across the screen (how I would like), but because they are 20 or so of them, they go off the page meaning you need to scroll across to view the rest.
Is there any way I can do this so that the cells cut off at the edge of the window and onto a new line?
To do this you will need to add a new row after a certain cut off point (i.e. certain number of columns)
In the example below I use a cut off of 5 :
string[] siteNames;
siteNames = client.GetListSiteElementNames();
// Number of cells on each row
int cutOffPoint = 5;
int currentNum = 1;
foreach (string siteName in siteNames) {
TableCell tempCell = new TableCell();
tempCell.Text = siteName;
if (currentNum > cutOffPoint) {
currentNum = 1;
// Code to add new row to table
}
this.sitesTableRow.Cells.Add(tempCell);
currentNum++;
}
Because I'm not sure what the this object is referring to or the sitesTableRow I can't include the code to add the row, but the idea would be that after 6 cells you would reset the currentNum variable and add a new row, and append the cells to this new row.
I have some drag and drop function where there are 8 items (dragArray) that can be dropped onto 2 big 'landing zones' (matchArray). But since I don't want them lie on top of each other, I've made an array where they are given positions (posArray).
var dragArray:Array = [drag_1, drag_2, drag_3, drag_4, drag_5, drag_6, drag_7, drag_8];
var matchArray:Array = [drop_1, drop_1, drop_1, drop_1, drop_2, drop_2, drop_2, drop_2];
var posArray:Array = [{x:412,y:246},{x:530,y:218},{x:431,y:186},{x:470,y:152},{x:140,y:111},{x:108,y:162},{x:179,y:210},{x:113,y:254}];
When all 8 items are dropped, a check button appears and I want to check if they are dropped onto the correct big landing zone. I tried using the following:
if (posArray[i].x != dragArray[i].x || dragArray[i].y != posArray[i].y )
But then, not only the landing zone must match, but the positions must also match.
When I use
if (matchArray[i].x != dragArray[i].x || dragArray[i].y != matchArray[i].y )
it doesn't work, because the positions of the (dragArray) items don't match with the registration points of the (matchArray) landing zones.
Is there any way of checking if the first 4 (drag_1, drag_2, drag_3, drag_4) items match with ANY of the first 4 posArray positions and the last 4 (drag_5, drag_6, drag_7, drag_8) match with ANY of the last 4 posArray positions?
If the goal is to check each element of one set against all elements of another set then you'll need to have two loops, one "nested" within the other. The general form of this algorithm in AS3 looks like
var allMatched:Boolean = true;
for(var i:Number=0; i<array1.length; i++)
{
var matchFound:Boolean = false;
for(var j:Number=0; j<array2.length; j++)
{
if(array1[i]==array2[j])
{
matchFound=true;
break; //exit the inner loop we found a match
}
}
if(!matchFound)
{
allMatched=false;
break; //we found an element in one set not present in the other, we can stop searching
}
}
if(allMatched)
trace("Everything from array1 was found somewhere in array2"); //For an element a in the set A there exists an element b in set B such that a = b
Let me know if this helps