This topic describes how to present document files during online meetings by using the online preview and editing feature in WebOffice.
Implementation overview
Integrate the online document preview and editing feature.
Use WebSocket to implement synchronized document sharing, including synchronization of real-time document operations by the presenter, document initialization for viewers, and synchronization of document type switching by the presenter.
Use the API operations of the
js-sdk
to listen for and synchronize presenter-related events and operations made by viewers.
Example
Implement synchronized sharing of Word files, spreadsheets, presentations, and PDF files.
Support synchronization of events such as page switching, scrolling, playback, and selection.
Share the meeting link to allow users to quickly join the meeting.
Frontend logic
The sample office-sync.ts file provides the front logic.
// Define a Presentation.
class Presentation {
wsServer: WebSocket
ins: any
app: any
meetingInfo: any
cacheInfo = {} // Cache document operations.
optionType = {
SLIDESHOWONNEXT: { name: 'SlideShowOnNext' }, // It is triggered by moving to the next slide in slideshow mode.
SLIDESHOWONPREVIOUS: { name: 'SlideShowOnPrevious' }, // It is triggered by moving to the previous slide in slideshow mode.
SLIDEMEDIACHANGED: { name: 'SlideMediaChanged' }, // It is triggered when media playback status changes
SLIDEPLAYERCHANGE: { name: 'SlidePlayerChange' }, // It is triggered when slideshow status changes.
SLIDELASERPENINKPOINTSCHANGED: { name: 'SlideLaserPenInkPointsChanged' }, // Send the ink of the leaser pointer.
SLIDESHOWBEGIN: { name: 'SlideShowBegin' }, // Start the slideshow.
SLIDESHOWEND: { name: 'SlideShowEnd' }, // End the slideshow.
SLIDEINKVISIBLE: { name: 'SlideInkVisible' }, // Whether to display annotations.
SLIDEINKTOOLBARVISIBLE: { name: 'SlideInkToolbarVisible' }, // Whether to use the leaser pointer and toolbar.
SLIDESELECTIONCHANGED: { name: 'SlideSelectionChanged' }, // The selection changes. This involves document synchronization in non-slideshow mode.
}
constructor(wsServer: WebSocket, meetingInfo: any, app: any, ins: any) {
this.wsServer = wsServer
this.meetingInfo = meetingInfo
this.app = app
this.ins = ins
}
// Initialize event listeners for presentation events by the presenter.
speakerInit() {
Object.keys(this.optionType).forEach(key => {
this.ins.ApiEvent.AddApiEventListener(this.optionType[key].name, async (e: any) => {
this.postMessage({ type: this.optionType[key].name, value: e })
this.cacheInfo[this.optionType[key].name] = e
// Hide right-click menus, hover effects, and links in slideshow mode
if (this.optionType[key].name === this.optionType.SLIDESHOWBEGIN.name) {
delete this.cacheInfo[this.optionType.SLIDESHOWEND.name]
await this._setMenusVisible(false)
} else if (this.optionType[key].name === this.optionType.SLIDESHOWEND.name) {
delete this.cacheInfo[this.optionType.SLIDESHOWBEGIN.name]
await this._setMenusVisible(true)
}
})
})
}
/**
* Push the presenter initialization message to viewers.
* @param isPlay Whether to use the slideshow mode.
*/
async sendInitInfo(isPlay?: boolean) {
// Automatically enter the slideshow mode after the file is initialized.
const params = {}
if (isPlay) {
params[this.optionType.SLIDESHOWBEGIN.name] = {}
this.cacheInfo[this.optionType.SLIDESHOWBEGIN.name] = {}
await this.app.ActivePresentation.SlideShowSettings.Run()
}
// Initializations are based on the cache settings, except for page numbers and animation settings.
const filterKey = [
this.optionType.SLIDESHOWONNEXT.name,
this.optionType.SLIDESHOWONPREVIOUS.name,
this.optionType.SLIDEPLAYERCHANGE.name,
]
Object.keys(this.cacheInfo).forEach(key => {
if (filterKey.indexOf(key) < 0) {
params[key] = this.cacheInfo[key]
}
})
this.postMessage({ type: 'init', value: params })
}
// Initialize viewer response actions.
async listenerInit(optionInfo: any) {
switch (optionInfo.type) {
case this.optionType.SLIDESHOWONNEXT.name: {
await this.app.ActivePresentation.SlideShowWindow.View.GotoNextClick()
break
}
case this.optionType.SLIDESHOWONPREVIOUS.name: {
await this.app.ActivePresentation.SlideShowWindow.View.GotoPreClick()
break
}
case this.optionType.SLIDEPLAYERCHANGE.name: {
if (
optionInfo.value.Data.action === 'switchTo' ||
(optionInfo.value.Data.action === 'effectFinish' && optionInfo.value.Data.isLastEffect)
) {
this._setPageAndAnimate(optionInfo)
}
break
}
case this.optionType.SLIDESHOWBEGIN.name:
await this.app.ActivePresentation.SlideShowSettings.Run()
await this._setMenusVisible(false)
break
case this.optionType.SLIDESHOWEND.name:
await this.app.ActivePresentation.SlideShowWindow.View.Exit()
await this._setMenusVisible(true)
break
case this.optionType.SLIDESELECTIONCHANGED.name: {
const playMode = await this.app.ActivePresentation.SlideShowWindow.View.State
if (playMode !== 'play') {
await this.app.ActivePresentation.SlideShowWindow.View.GotoSlide(
optionInfo.value,
)
}
break
}
case this.optionType.SLIDEINKVISIBLE.name:
this.app.ActivePresentation.SlideShowWindow.View.PointerVisible =
optionInfo.value.Data.showmark
break
case this.optionType.SLIDELASERPENINKPOINTSCHANGED.name:
// Directly make a call after receiving callback data when the system detects laser pointer ink events.
await this.app.ActivePresentation.SlideShowWindow.View.SetLaserPenData({
Data: optionInfo.value.Data,
})
break
case this.optionType.SLIDEINKTOOLBARVISIBLE.name:
this.app.ActivePresentation.SlideShowWindow.View.MarkerEditVisible =
optionInfo.value.Data.show
break
case this.optionType.SLIDEMEDIACHANGED.name:
await this.app.ActivePresentation.SlideShowWindow.View.SetMediaObj({
Data: optionInfo.value.Data,
})
break
default:
break
}
}
/**
* Push messages.
* @param data The message.
*/
postMessage = (data: any) => {
const { meetId, user } = this.meetingInfo
this.wsServer.send(JSON.stringify({ id: user.id, meetId, data }))
}
// Specify whether to display right-click menus, hover effects, and links in slideshow mode.
async _setMenusVisible(flag: boolean) {
const linkTip=app.Enum.PpToolType.pcPlayHoverLink // Represents the hover link tool.
const imageTip=app.Enum.PpToolType.pcImageHoverTip // Represents the hover image tool.
const menu = this.app.Enum.PpToolType.pcPlayingMenu // Represents the right-click menu.
await this.app.ActivePresentation.SlideShowWindow.View.SetToolVisible(linkTip, flag)
await this.app.ActivePresentation.SlideShowWindow.View.SetToolVisible(imageTip, flag)
await this.app.ActivePresentation.SlideShowWindow.View.SetToolVisible(menu, flag)
}
/**
* Configure synchronization between slide and animate.
* @param optionInfo The synchronization information.
*/
async _setPageAndAnimate(optionInfo) {
// try {
const slideIndex =
await this.app.ActivePresentation.SlideShowWindow.View.Slide.SlideIndex
const clickIndex =
await this.app.ActivePresentation.SlideShowWindow.View.GetClickIndex()
if (slideIndex !== optionInfo.value.Data.slideIndex + 1) {
await this.app.ActivePresentation.SlideShowWindow.View.GotoSlide(
optionInfo.value.Data.slideIndex + 1,
)
}
if (clickIndex !== optionInfo.value.Data.animateIndex + 1) {
await this.app.ActivePresentation.SlideShowWindow.View.GotoClick(
optionInfo.value.Data.animateIndex + 2,
)
}
// } catch (e) {
// console.error(e)
// }
}
}
// PDF object
class Pdf {
wsServer: WebSocket
ins: any
meetingInfo: any
app: any
optionType = {
ZOOM: { name: 'ZoomUpdated' }, // Zoom.
SCROLL: { name: 'Scroll' }, // Scroll.
PAGECHANGE: { name: 'CurrentPageChange' }, // Page switching.
PAGESTARTPLAY: { name: 'StartPlay' }, // Start the playback.
PAGEENDPLAY: { name: 'EndPlay' }, // End the playback.
}
cacheScroll: any
constructor(wsServer: WebSocket, meetingInfo: any, app: any, ins: any) {
this.wsServer = wsServer
this.meetingInfo = meetingInfo
this.app = app
this.ins = ins
}
// Initialize event listeners for PDF events by the presenter.
speakerInit() {
Object.keys(this.optionType).forEach(key => {
this.ins.ApiEvent.AddApiEventListener(this.optionType[key].name, async (e: any) => {
let message = e
if (this.optionType[key].name === this.optionType.SCROLL.name) {
const zoom = await this.app.ActivePDF.Zoom
message = { scroll: e, zoom }
} else if (this.optionType[key].name === this.optionType.ZOOM.name) {
const scroll = await this.app.ActivePDF.Scroll
message = { scroll, zoom: e }
}
this.postMessage({ type: this.optionType[key].name, value: message })
})
})
}
async sendInitInfo() {
const zoom = await this.app.ActivePDF.Zoom
const scroll = await this.app.ActivePDF.Scroll
const playMode = await this.app.ActivePDF.PlayMode
this.postMessage({
type: 'init',
value: {
[this.optionType.ZOOM.name]: { zoom, scroll },
...(playMode
? { [this.optionType.PAGESTARTPLAY.name]: true }
: { [this.optionType.PAGEENDPLAY.name]: true }),
},
})
}
// Initialize viewer response actions.
async listenerInit(optionInfo: any) {
switch (optionInfo.type) {
case this.optionType.SCROLL.name: {
const zoom = await this.app.ActivePDF.Zoom
const x = (optionInfo.value.scroll.ScrollX / optionInfo.value.zoom) * zoom
const y = (optionInfo.value.scroll.ScrollY / optionInfo.value.zoom) * zoom
this.app.ActivePDF.ScrollTo(x, y)
this.cacheScroll = { x, y }
break
}
case this.optionType.ZOOM.name:
this.app.ActivePDF.Zoom = optionInfo.value.zoom
// Set the scroll position after zooming.
this.app.ActivePDF.ScrollTo(optionInfo.value.scroll.x, optionInfo.value.scroll.y)
break
case this.optionType.PAGECHANGE.name: {
// Determine page change based on scrolling. This is effective only in non-continuous page mode.
setTimeout(async () => {
const scroll = await this.app.ActivePDF.Scroll
if (
Math.abs(scroll.x - this.cacheScroll.x) < 1 &&
Math.abs(scroll.y - this.cacheScroll.y) < 1
) {
return
}
this.app.ActivePDF.JumpToPage({ PageNum: optionInfo.value + 1 })
})
break
}
case this.optionType.PAGESTARTPLAY.name:
this.app.ActivePDF.StartPlay('active', true, true)
break
case this.optionType.PAGEENDPLAY.name:
this.app.ActivePDF.EndPlay()
break
default:
break
}
}
// Push messages.
postMessage = (data: any) => {
const { meetId, user } = this.meetingInfo
this.wsServer.send(JSON.stringify({ id: user.id, meetId, data }))
}
}
// Document object
class Writer {
wsServer: WebSocket
meetingInfo: any
app: any
ins: any
cacheInfo = {} // Cache document operations.
optionType = {
WINDOWSCROLLCHANGE: { name: 'WindowScrollChange' }, // A scroll notification.
WINDOWSELECTIONCHANGE: { name: 'WindowSelectionChange' }, // A selection change notification.
}
constructor(wsServer: WebSocket, meetingInfo: any, app: any, ins: any) {
this.wsServer = wsServer
this.meetingInfo = meetingInfo
this.app = app
this.ins = ins
}
// Initialize event listeners for the events by the presenter.
speakerInit() {
Object.keys(this.optionType).forEach(key => {
this.ins.ApiEvent.AddApiEventListener(this.optionType[key].name, async (e: any) => {
this.cacheInfo[this.optionType[key].name] = e
this.postMessage({ type: this.optionType[key].name, value: e })
})
})
}
async sendInitInfo() {
this.postMessage({
type: 'init',
value: this.cacheInfo,
})
}
// Initialize viewer response actions.
async listenerInit(optionInfo: any) {
switch (optionInfo.type) {
case this.optionType.WINDOWSCROLLCHANGE.name: {
const range = await this.app.ActiveDocument.ActiveWindow.RangeFromPoint(
optionInfo.value.Data.scrollLeft,
optionInfo.value.Data.scrollTop,
)
this.app.ActiveDocument.ActiveWindow.ScrollIntoView(range)
break
}
case this.optionType.WINDOWSELECTIONCHANGE.name: {
// Set the selection range to zero when a point is clicked, indicating no text is selected.
if (optionInfo.value.isPoint) {
this.app.ActiveDocument.Range.SetRange(0, 0) // Start: number, End: number
} else {
this.app.ActiveDocument.Range.SetRange(
optionInfo.value.begin,
optionInfo.value.end,
) // Start: number, End: number
}
break
}
default:
break
}
}
// Push messages.
postMessage = (data: any) => {
const { meetId, user } = this.meetingInfo
this.wsServer.send(JSON.stringify({ id: user.id, meetId, data }))
}
}
// Sheet object.
class Sheet {
wsServer: WebSocket
meetingInfo: any
app: any
ins: any
optionType = {
WORKSHEET_SELECTIONCHANGE: { name: 'Worksheet_SelectionChange' }, // Selection changes.
WORKSHEET_SCROLLCHANGE: { name: 'Worksheet_ScrollChange' }, // Scroll.
WORKSHEET_FORCELANDSCAPE: { name: 'Worksheet_ForceLandscape' }, // Notification of forced landscape mode.
}
constructor(wsServer: WebSocket, meetingInfo: any, app: any, ins: any) {
this.wsServer = wsServer
this.meetingInfo = meetingInfo
this.app = app
this.ins = ins
}
// Initialize event listeners for the sheet events by the presenter.
speakerInit() {
Object.keys(this.optionType).forEach(key => {
this.ins.ApiEvent.AddApiEventListener(this.optionType[key].name, async (e: any) => {
if (this.optionType[key].name === this.optionType.WORKSHEET_SCROLLCHANGE.name) {
const scrollColumn = await this.app.ActiveWindow.ScrollColumn
const scrollRow = await this.app.ActiveWindow.ScrollRow
this.postMessage({
type: this.optionType[key].name,
value: { column: scrollColumn, row: scrollRow },
})
} else if (this.optionType[key].name === this.optionType.WORKSHEET_SELECTIONCHANGE.name) {
// Obtain the constituency information.
const params = await this._getSelectionData()
this.postMessage({ type: this.optionType[key].name, value: params })
} else {
this.postMessage({ type: this.optionType[key].name, value: e })
}
})
})
}
async sendInitInfo() {
const scrollColumn = await this.app.ActiveWindow.ScrollColumn
const scrollRow = await this.app.ActiveWindow.ScrollRow
const params = await this._getSelectionData()
this.postMessage({
type: 'init',
value: {
[this.optionType.WORKSHEET_SCROLLCHANGE.name]: { column: scrollColumn, row: scrollRow },
[this.optionType.WORKSHEET_SELECTIONCHANGE.name]: params,
},
})
}
// Initialize viewer response actions.
async listenerInit(optionInfo: any) {
switch (optionInfo.type) {
case this.optionType.WORKSHEET_SELECTIONCHANGE.name:
await this.app.Sheets(optionInfo.value.index).Activate()
if (optionInfo.value.selection.type === 'shape') {
await this.app.ActiveSheet.Shapes.Item(1).Select()
} else {
await this.app.Range(optionInfo.value.selection.value).Select() // Select multiple cells.
}
break
case this.optionType.WORKSHEET_SCROLLCHANGE.name:
this.app.ActiveWindow.ScrollColumn = optionInfo.value.column
this.app.ActiveWindow.ScrollRow = optionInfo.value.row
break
case this.optionType.WORKSHEET_FORCELANDSCAPE.name:
// await WPSOpenApi.Application.Range('C47:D54'). Select() // Select multiple cells.
this.app.ActiveDocument.Range.SetRange(
optionInfo.value.begin,
optionInfo.value.end,
) // Start: number, End: number
break
default:
break
}
}
async _getSelectionData() {
const params: {
value: string
type: string
} = {
value: '',
type: '',
}
const shapes = await this.app.Selection.Item(1)
// Determine whether the selected content is shapes or cells.
if (shapes) {
params.value = await shapes.ID
params.type = 'shape'
} else {
const selection = await this.app.Selection.Address()
params.value = selection
params.type = 'cell'
}
// Obtain the sheet number of the active sheet.
const sheetIndex = await this.app.ActiveSheet.Index
// Obtain the active cells.
const activitiItem = {
row: await this.app.Selection.Row,
col: await this.app.Selection.Column,
}
return { index: sheetIndex, selection: params, activitiItem }
}
// Push messages.
postMessage = (data: any) => {
const { meetId, user } = this.meetingInfo
this.wsServer.send(JSON.stringify({ id: user.id, meetId, data }))
}
}
export { Presentation, Pdf, Writer, Sheet }
A presenter accesses the document in editing mode. The following lines define the major logic:
// The ins object is obtained from aliyun.config.
// ...
await ins.ready();
let app = ins.Application
let webSocket = {
send: data => {
console.log('=====Sent syncOpt', data)
// socket.emit('syncOpt', data)
},
}
let meetingInfo = { meetId: meetingId, user: { id: userId } }
let syncIns
if (suffix == '.docx') {
syncIns = new Writer(webSocket, meetingInfo, app, ins)
} else if (suffix == '.pptx') {
syncIns = new Presentation(webSocket, meetingInfo, app, ins)
} else if (suffix == '.xlsx') {
syncIns = new Sheet(webSocket, meetingInfo, app, ins)
} else if (suffix == '.pdf') {
syncIns = new Pdf(webSocket, meetingInfo, app, ins)
}
syncIns.speakerInit()
Viewers access the document in read-only mode. The following lines define the major logic:
// The ins object is obtained from aliyun.config.
// ...
await ins.ready()
let app = ins.Application
let webSocket = {
send: data => {
console.log('=====Sent syncOpt', data)
// socket.emit('syncOpt', data)
},
}
let meetingInfo = { meetId: meetingId, user: { id: userId } }
let syncIns
if (suffix == '.docx') {
syncIns = new Writer(webSocket, meetingInfo, app, ins)
} else if (suffix == '.pptx') {
syncIns = new Presentation(webSocket, meetingInfo, app, ins)
} else if (suffix == '.xlsx') {
syncIns = new Sheet(webSocket, meetingInfo, app, ins)
} else if (suffix == '.pdf') {
syncIns = new Pdf(webSocket, meetingInfo, app, ins)
}
socket.on('syncOpt', message => {
let data = JSON.parse(message)
console.log('=====Received syncOpt', data)
console.log('----- app and syncIns before listening', app, syncIns)
syncIns.listenerInit(data)
})