Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't Seem To Use Ranges across calls to Word.run(context => {...}) in Office JS

My project involves parsing a word document to extract a set of tokens then allowing the user to selectively perform bulk actions on those tokens. Because of the UI step, I need to be able to call myRange.select() and myRange.font.set(...) on a subsequent Word.run(ctx => {...}) call from run which extracted the tokens.

If this is more appropriate as a stackoverflow post, I apologize and will repost there but it appears that the library is not matching the API, as I understand it. I could definitely be mistaken though.

Expected Behavior

I expect that calling Word.run(ctx => myRange.select()) would cause that range to be selected as that range was added to context.trackedObjects on a previous run.

Current Behavior

Nothing happens, not even an error to the console.

note that the commented code of keeping the context object on the Chunk class and using that for subsequent runs will work on Word Online / Chrome but does not work in Windows or OSX Word

Your Environment

  • Platform [PC desktop, Mac, iOS, Office Online]: Win11 desktop, OSX, Word Online (all most recent)
  • Host [Excel, Word, PowerPoint, etc.]: Word
  • Office version number: most recent
  • Operating System: OSX / Win11
  • Browser (if using Office Online): Chrome

Code:

import * as React from 'react'
import { Container, ListGroup, ListGroupItem, Button, Label, Input, ButtonGroup, Row } from 'reactstrap'

class Chunk {
    range: Word.Range
    text: string
    // context: Word.RequestContext
    constructor(t: string, r: Word.Range, ctx: Word.RequestContext) {
        this.range = r
        this.text = t
        ctx.trackedObjects.add(r)
        r.track()
        // this.context = ctx
    }
    async select(ctx: Word.RequestContext) {
        console.log('select')
        this.range.select('Select')
        ctx.sync()
    }
}

const getChunks = async () => {
    return Word.run(async context => {
        let paragraphs = context.document.body.paragraphs.load()
        let wordRanges: Array<Word.RangeCollection> = []
        await context.sync()
        paragraphs.items.forEach(paragraph => {
            const ranges = paragraph.getTextRanges([' ', ',', '.', ']', ')'], true)
            ranges.load('text')
            wordRanges.push(ranges)
        })
        await context.sync()
        let chunks: Chunk[] = []
        wordRanges.forEach(ranges => ranges.items.forEach(range => {
            chunks.push(new Chunk(range.text, range, context))
        }))
        await context.sync()
        return chunks
    })

}

interface ChunkControlProps { chunk: Chunk; onSelect: (e: React.MouseEvent<HTMLElement>) => void }
export const ChunkControl: React.SFC<ChunkControlProps> = ({ chunk, onSelect}) => {
    return (
        <div style={{marginLeft: '0.5em'}}><a href='#' onClick={onSelect}>{chunk.text}</a></div>
    )
}
export class App extends React.Component<{title: string}, {chunks: Chunk[]}> {
    constructor(props, context) {
        super(props, context)
        this.state = { chunks: [] }
    }

    componentDidMount() { this.click() }

    click = async () => {
        const chunks = await getChunks()
        this.setState(prev => ({ ...prev, chunks: chunks }))
    }

    onSelectRange(chunk: Chunk) {
        return async (e: React.MouseEvent<HTMLElement>) => {
            e.preventDefault()
            Word.run(ctx => chunk.select(ctx))
        }
    }

    render() {
        return (
            <Container fluid={true}>
                <Button color='primary' size='sm' block className='ms-welcome__action' onClick={this.click}>Find Chunks</Button>
                <hr/>
                <ListGroup>
                    {this.state.chunks.map((chunk, idx) => (
                        <ListGroupItem key={idx}>
                            <ChunkControl  onSelect={this.onSelectRange(chunk)} chunk={chunk}/>
                        </ListGroupItem>
                    ))}
                </ListGroup>
            </Container>
        )
    };
};

Version that works on WordOnline but not Windows or OSX:

(duplicate code from above elided)

 class Chunk {
    range: Word.Range
    text: string
    context: Word.RequestContext
    constructor(t: string, r: Word.Range, ctx: Word.RequestContext) {
        this.range = r
        this.text = t
        this.context = ctx
    }
    async select() {
        this.range.select('Select')
        ctx.sync()
    }
}

const getChunks = async () => {
    return Word.run(async context => {
        ...
    })

}

...

export class App extends React.Component<{title: string}, {chunks: Chunk[]}> {
    constructor(props, context) {
        super(props, context)
        this.state = { chunks: [] }
    }

    componentDidMount() { this.click() }

    click = async () => {
        const chunks = await getChunks()
        this.setState(prev => ({ ...prev, chunks: chunks }))
    }

    onSelectRange(chunk: Chunk) {
        return async (e: React.MouseEvent<HTMLElement>) => {
            e.preventDefault()
            chunk.select()
        }
    }

    render() {
        return (
            <Container fluid={true}>
                <Button color='primary' size='sm' block className='ms-welcome__action' onClick={this.click}>Find Chunks</Button>
                <hr/>
                <ListGroup>
                    {this.state.chunks.map((chunk, idx) => (
                        <ListGroupItem key={idx}>
                            <ChunkControl  onSelect={this.onSelectRange(chunk)} chunk={chunk}/>
                        </ListGroupItem>
                    ))}
                </ListGroup>
            </Container>
        )
    };
};
like image 802
max Avatar asked Feb 21 '18 17:02

max


Video Answer


1 Answers

In terms of the general pattern: The trick is to do a Word.run just like you normally would, but instead of having it create a new anonymous request context, have the run resume using the context of some existing object. To resume using an existing context, you simply use one of the function overloads available off of Word.run; namely, an overload that takes in an object (or array of objects) as the first argument, and the batch as the second:

Code sample

Note that in order to be able to use the Range object across different run-s, you will have needed to call range.track() to prolong its lifetime before the finish of the first run; and you should clean it up at some point using range.untrack().

The text above, and the code sample, comes from my book Building Office Add-ins using Office.js. There is a bunch more info there as well. Pasting in one section in particular:

What happens when you forget to pass in a Range object

Related sections of the book that you might find useful:

TOC

As for your observation about a difference in behavior between Online and Windows/Mac -- that's quite interesting, and sounds like a bug that should be investigated. Would you mind filing a bug for just that particular aspect, with a minimal repro, at https://github.com/OfficeDev/office-js/issues? (Sorry for sending you back and forth, I know you'd already been there at https://github.com/OfficeDev/office-js/issues/68; but I want to separate out the conceptual issue which was really just a question (and documentation issue), versus a possible bug as seen in the difference of behavior).

Best!

~ Michael

like image 59
Michael Zlatkovsky - Microsoft Avatar answered Sep 29 '22 09:09

Michael Zlatkovsky - Microsoft