1292681219
Change-Id: I5bafdb149d9de7255c8c038af6c495706158a7fb Reviewed-on: https://gerrit.libreoffice.org/81023 Reviewed-by: Szymon Kłos <szymon.klos@collabora.com> Tested-by: Szymon Kłos <szymon.klos@collabora.com>
406 lines
17 KiB
Text
406 lines
17 KiB
Text
Leaflet platform for LibreOffice Online
|
|
========================================
|
|
|
|
This is the client part of LibreOffice Online. For the server part, see the
|
|
../wsd/README, and install it first.
|
|
|
|
Build dependencies
|
|
------------------
|
|
|
|
npm is provided by the nodejs package.
|
|
|
|
npm should be at least 2.14, if not 3.x. Use `npm -v` to find the current version.
|
|
Upgrade npm (as root):
|
|
|
|
npm install -g npm
|
|
|
|
Another way is to use npm as a user, but set its prefix to a directory where
|
|
you have write access. If you want that, you need to have an ~/.npmrc with the
|
|
line e.g.
|
|
|
|
prefix=/opt/npm
|
|
|
|
All of the dependency tree is locked in the repository, so there is no need to fetch
|
|
data from npm registry at all. An npm install will use the tarballs in node_shrinkwrap/
|
|
directory, so build process can move forward even without an internet connection.
|
|
|
|
To update any of the dependency, process often goes like this:
|
|
|
|
1.) Change version of the dependency in package.json
|
|
2.) Do an `npm update <package_name>', which fetches the new module from npm registry. Make sure that
|
|
only the module that you want to update is updated.
|
|
3.) `npm shrinkwrap --dev` to update npm-shrinkwrap.json with newer information. You might see
|
|
that this tool updates path convention of tarballs inside node_shrinkwrap/ also, but this
|
|
should get corrected in next step.
|
|
4.) Do `shrinkpack', which should remove the old tarball, add new one, and make appropriate
|
|
changes to npm-shrinkwrap.json (removing path convention changes by step 3).
|
|
|
|
If you have not installed `shrinkpack' globally using `npm install -g shrinkpack@next', it should be
|
|
in node_modules/.bin/shrinkpack, so you can use the binary from there.
|
|
|
|
Building
|
|
--------
|
|
|
|
Just do:
|
|
|
|
make
|
|
|
|
Above step would create a non-minified bundle.js and admin-bundle.js without source-maps in dist/ for main loleaflet and admin console
|
|
respectively.
|
|
|
|
To build with debug-info, i.e with sourcemaps:
|
|
|
|
make DEBUG=true
|
|
|
|
Above will produce source-map files, bundle.js.map and admin-bundle.js.map, for bundle.js and admin-bundle.js respectively.
|
|
It will also link existing bundle.js and admin-bundle.js to their map files by adding a sourceMappingURL to them. While debugging,
|
|
these .map files will be fetched from the server if present in dist/, otherwise there is no way to debug while browsing without these
|
|
source-map files.
|
|
|
|
To minify our bundle.js and admin-bundle.js passing a MINIFY=true flag to 'make' will minify it. This can be helpful in production
|
|
environments.
|
|
|
|
make MINIFY=true
|
|
|
|
'make dist' forces minifying.
|
|
|
|
Running
|
|
-------
|
|
|
|
To see an example:
|
|
|
|
* run loolwsd, like:
|
|
|
|
./loolwsd --o:sys_template_path=${SYSTEMPLATE} --o:child_root_path=${ROOTFORJAILS}
|
|
|
|
Note that this will, by default, set the loolwsd's file server's root to the parent directory of loolwsd,
|
|
which means you can access all the files in loleaflet using /loleaflet/ path. It is advised to set
|
|
file_server_root_path manually for more control. See wsd/README for more information.
|
|
|
|
* open dist/loleaflet.html through loolwsd's fileserver
|
|
|
|
https://localhost:9980/loleaflet/dist/loleaflet.html?file_path=file:///PATH/TO_DOC&host=wss://localhost:9980
|
|
|
|
and you should see the document in the browser.
|
|
Note that accessing local storage is disabled by default for security reasons. You need to explicitly enable it
|
|
with the --o:storage.filesystem[@allow]=true option of loolwsd. In case anything goes wrong, check the loolwsd console for
|
|
the debugging output. You might be asked to confirm the certificate if you are using self-signed certificate
|
|
for loolwsd.
|
|
|
|
Development
|
|
-----------
|
|
|
|
For faster development you might want to install browser-sync which can reload app in your browsers automatically
|
|
after you edit and save the source code (so for .css only):
|
|
|
|
npm install -g browser-sync
|
|
|
|
Remember to add '--enable-browsersync' argument for './configure' script and delete `dist` directory.
|
|
|
|
Run server and start browser-sync:
|
|
|
|
LOOL_SERVE_FROM_FS=1 make run
|
|
|
|
and in the second terminal:
|
|
|
|
make sync-[writer|calc|impress]
|
|
|
|
Your browser will open example document and now you can edit .css files and see the result without server restart.
|
|
|
|
To run another document use:
|
|
browser-sync start --config browsersync-config.js --startPath "loleaflet/96c23f663/loleaflet.html?file_path=file:///path/to/the/file.ods"
|
|
|
|
Admin Panel
|
|
-----------
|
|
|
|
You can do live monitoring of all the user sessions running on loolwsd instance. To access the admin
|
|
console you need to ask for admin.html file from loolwsd which resides in dist/admin/admin.html.
|
|
|
|
For example:
|
|
|
|
https://localhost:9980/loleaflet/dist/admin/admin.html
|
|
|
|
It will ask for username and password which is set by the admin_console options of loolwsd. For example you can
|
|
pass --o:admin_console.username=admin --o:admin_console.password=admin in command line, or set these values in
|
|
loolwsd.xml. After entering the correct password you should be able to monitor the live documents opened, total
|
|
users, memory consumption, document URLs with number of users viewing that document etc. You can also kill the
|
|
documents directly from the panel which would result in closing the socket connection to the respective document.
|
|
|
|
Testing
|
|
-------
|
|
- to run the unit tests
|
|
+ open spec/loleaflet.html in the browser
|
|
- to simulate an editing session and to get the tile loading times
|
|
+ open spec/tilebench.html in the browser
|
|
- to simulate a client opening several documents in the browser
|
|
+ open spec/loadtest.html in the browser
|
|
- to simulate a client opening several documents in the console
|
|
+ run: node_modules/.bin/mocha spec/headlessLoadTest.js
|
|
- to simulate multiple clients opening several documents in the console
|
|
+ run: make load-test
|
|
|
|
The structure of the unit tests:
|
|
Loleaflet unit tests are located under spec/loleaflet. Following Leaflet's testing style, each test's path
|
|
should mirror the source's path, so spec/loleaflet/control/ToolbarSpec.js tests the features from
|
|
src/control/Toolbar.js . Any new test file needs also to be added in spec/leaflet.html
|
|
|
|
|
|
API & events
|
|
------------
|
|
|
|
#######################################################################
|
|
# See /loleaflet/reference.html for a better formatted documentation. #
|
|
# See /wsd/reference.md for the HTTP API documentation. #
|
|
#######################################################################
|
|
|
|
Search:
|
|
- API:
|
|
map.search(text, [backward])
|
|
map.higlightAll(text)
|
|
- events:
|
|
map.on('search', function (e) {}) (currently only fired when no search result is found) where:
|
|
+ e.originalPhrase = the phrase that has been searched for
|
|
+ e.count = number of results
|
|
+ e.results = [SearchResult], where SearchResult = {part: part, rectangles: [Bounds]}
|
|
|
|
Zoom:
|
|
- API:
|
|
map.zoomIn(amount)
|
|
map.zoomOut(amount)
|
|
map.getMinZoom()
|
|
map.getMaxZoom()
|
|
- events:
|
|
map.on('zoomend zoomlevelschange', function)
|
|
|
|
Edit, view, readOnly:
|
|
- API:
|
|
map.setPermission('edit' | 'view' | 'readonly')
|
|
- events:
|
|
map.on('updatepermission', function (e) {}) where:
|
|
+ e.perm == 'edit' | 'view' | 'readonly'
|
|
|
|
Buttons like Bold, Italic, Strike through etc.
|
|
- API:
|
|
map.toggleCommandState('.uno:' + 'Bold' | 'Italic' | 'Underline' | 'Strikeout' |
|
|
'LeftPara' | 'CenterPara' | 'RightPara' | 'JustifyPara' |
|
|
'IncrementIndent' | 'DecrementIndent'
|
|
- events:
|
|
map.on('commandstatechanged', function (e) {}) where:
|
|
+ e.commandName == '.uno:' + 'Bold' | 'Italic' | 'StyleApply' | 'CharFontName' | 'FontHeight' etc.
|
|
+ e.state = 'true' | 'false'
|
|
+ e.state = fontName | fontSize | styleName
|
|
map.on('commandresult', function (e) {}) where:
|
|
+ e.commandName == '.uno:' + 'Bold' | 'Italic' | 'StyleApply' | 'CharFontName' | 'FontHeight' etc.
|
|
+ e.success = true | false | undefined
|
|
|
|
Parts (like slides in presentation, or sheets in spreadsheets):
|
|
- API:
|
|
map.setPart('next' | 'prev' | partNumber)
|
|
map.getNumberOfParts()
|
|
map.getCurrentPartNumber()
|
|
map.getPreview(id, index, maxWidth, maxHeight, [options], forAllClients)
|
|
+ id = the ID of the request so that the response can be identified
|
|
+ index = the part / page 's number
|
|
+ maxWidth / maxHeight = max dimensions so that the ratio is preserved
|
|
+ options = {autoUpdate: <boolean>, broadcast: <boolean>} -
|
|
+ autoUpdate - boolean, automatically updates the previews
|
|
+ broadcast - boolean, whether the response (a preview of a slide) should be sent to all clients
|
|
viewing the same presentation
|
|
map.getCustomPreview(id, part, width, height, tilePosX, tilePosY, tileWidth, tileHeight, [options])
|
|
+ id = the ID of the request so that the response can be identified
|
|
+ part = the part containing the desired preview
|
|
+ width / height = the preview's size in pixels
|
|
+ tilePosX / tilePosY = the rectangles's starting position in twips
|
|
+ tileWidth / tileHeight = the rectangle's dimension in twips
|
|
+ options = {autoUpdate: true} - automatically updates the previews
|
|
map.removePreviewUpdate(id)
|
|
+ id = the preview's id
|
|
|
|
- events:
|
|
map.on('updateparts', function (e) {}) where:
|
|
+ e.selectedPart is the current part
|
|
+ e.parts == the number of parts that the document has
|
|
+ e.docType == 'text' | 'spreadsheet' | 'presentation' | 'drawing' | 'other'
|
|
+ [e.partNames] if present, part names (e.g. sheet names)
|
|
map.on('tilepreview', function (e) {}) where:
|
|
+ e.tile - the preview image
|
|
+ e.id - the preview id
|
|
+ e.width - width of the image
|
|
+ e.height - height of the image
|
|
+ [e.part] - if the preview is for a part
|
|
+ e.docType
|
|
|
|
Statusindicator (when the document is loading):
|
|
- events
|
|
map.on('statusindicator', function (e) {}) where:
|
|
+ e.statusType = 'start' | 'setvalue' | 'finish' | 'loleafletloaded' | 'alltilesloaded'
|
|
+ e.value == a value from 0 to 100 indicating the status
|
|
if the statusType is 'setvalue
|
|
+ 'loleafletloaded' is fired when the JS code is initialized and the document
|
|
load request is sent and we're waiting for the tiles
|
|
+ 'alltilesloaded' is fired when all newly requested (empty tiles) have been loaded
|
|
it is not fired during pre-fetching and during editing
|
|
|
|
Save:
|
|
- API:
|
|
map.saveAs(url, [format, options])
|
|
map.downloadAs(name, [format, options])
|
|
|
|
Scroll (the following are measured in pixels):
|
|
- API:
|
|
+ options = An object with members: update (type: Boolean, default: false)
|
|
like {update: true}
|
|
map.scroll(x,y, options)
|
|
+ scroll right by 'x' and down by 'y' (or left and up if negative)
|
|
map.scrollDown(y, options)
|
|
+ scroll down by 'y' (or up if negative)
|
|
map.scrollRight(x, options)
|
|
+ scroll right by 'x' (or left if negative)
|
|
map.scrollTop(y, options)
|
|
+ scroll to 'y' offset relative to the beginning of the document
|
|
map.scrollLeft(x, options)
|
|
+ scroll to 'x' offset relative to the beginning of the document
|
|
map.scrollOffset()
|
|
+ returns the scroll offset relative to the beginning of the document
|
|
map.getDocSize()
|
|
+ returns the document's size in pixels
|
|
map.getDocType()
|
|
+ returns 'text' | 'spreadsheet' | 'presentation' | 'drawing' | 'other'
|
|
- events
|
|
map.on('docsize', function (e) {}) where:
|
|
+ e.x = document width
|
|
+ e.y = document height
|
|
map.on('updatescrolloffset', function (e) {}) where:
|
|
+ e.x = difference between document's left and current view's left
|
|
(how much has the document been scrolled right)
|
|
+ e.y = difference between document's top and current view's top
|
|
(how much has the document been scrolled down)
|
|
- this event is fired when zooming and the current view is maintained but the
|
|
document shrinks or grow OR when the document is panned OR when the container is resized
|
|
map.on('scrollto', function (e) {}) where:
|
|
+ e.x = view's left position (so that the cursor/search result is in the center)
|
|
+ e.y = view's top position (so that the cursor/search result is in the center)
|
|
map.on('scrollby', function (e) {}) where:
|
|
+ e.x = the amount scrolled to the right (or left if negative)
|
|
+ e.y = the amount scrolled to the bottom (or top if negative)
|
|
|
|
Writer pages:
|
|
- API:
|
|
map.goToPage(page)
|
|
map.getNumberOfPages()
|
|
map.getCurrentPageNumber()
|
|
map.getPreview(id, index, maxWidth, maxHeight, [options])
|
|
+ id = the ID of the request so that the response can be identified
|
|
+ index = the part / page 's number
|
|
+ maxWidth / maxHeight = max dimensions so that the ratio is preserved
|
|
+ options = {autoUpdate: true} - automatically updates the previews
|
|
map.getCustomPreview(id, part, width, height, tilePosX, tilePosY, tileWidth, tileHeight, [options])
|
|
+ id = the ID of the request so that the response can be identified
|
|
+ part = the part containing the desired preview
|
|
+ width / height = the preview's size in pixels
|
|
+ tilePosX / tilePosY = the rectangles's starting position in twips
|
|
+ tileWidth / tileHeight = the rectangle's dimension in twips
|
|
+ options = {autoUpdate: true} - automatically updates the previews
|
|
map.removePreviewUpdate(id)
|
|
+ id = the preview's id
|
|
map.getPageSizes()
|
|
+ returns {twips: [Bounds], pixels: [Bounds]}
|
|
|
|
- events
|
|
map.on('pagenumberchanged', function (e) {}) where:
|
|
+ e.currentPage = the page on which the cursor lies
|
|
+ e.pages = number of pages
|
|
+ e.docType = document type, should be 'text'
|
|
|
|
Error:
|
|
- events
|
|
map.on('error', function (e) {}) where
|
|
+ [e.msg] = a message describing the error
|
|
+ [e.cmd] = the command that caused the error
|
|
+ [e.kind] = the kind of error
|
|
|
|
Infobars:
|
|
- events
|
|
map.on('infobar', function (e) {}) where
|
|
+ [e.msg] = a message
|
|
+ [e.actionlabel] = Label for the action button
|
|
+ [e.action] = A link (starting with http).
|
|
Please extend to allow other actions when needed.
|
|
|
|
CommandValues:
|
|
- api:
|
|
map.getToolbarCommandValues(command)
|
|
+ returns a JSON mapping of all possible values for the command
|
|
map.applyFont(fontName)
|
|
map.applyFontSize(fontSize)
|
|
map.applyStyle(style, styleFamily)
|
|
- events
|
|
map.on('updatetoolbarcommandvalues', function (e) {}) where
|
|
+ e.commandName = '.uno:StyleApply', etc
|
|
+ e.commandValues = a JSON mapping of all possible values for the command
|
|
|
|
Print:
|
|
- events
|
|
map.on('print', function (e) {}) where
|
|
+ e.url = file download url
|
|
|
|
Contributing
|
|
------------
|
|
|
|
Code conventions:
|
|
|
|
* files should have unix line terminators (LF)
|
|
* tools to convert files: dos2unix or fromdos
|
|
|
|
Implementation details
|
|
----------------------
|
|
|
|
Loading a document:
|
|
The map should have the following options:
|
|
- server address
|
|
- doc - path to the document that will be loaded
|
|
- edit = the initial permission
|
|
- readOnly - whether the document is read only
|
|
- [timestamp] - optionally provided for remote documents
|
|
|
|
How zooming works:
|
|
The zoom level goes from 1 to 20 (those limits can be changed) and the initial
|
|
level is 10, which represents the 100% zoom level. The zoom factor is 1.2
|
|
|
|
Controls are added above the map in a div called "controls" is intended to be used as a toolbar.
|
|
There is no leaflet method of adding them in a separate div, so for now this is done in the html
|
|
document after the map initialization.
|
|
|
|
To enable scrollbars the map is placed above a div that contains a bigger div of
|
|
the document's size (a mock document). So the div under the map gets scrollbars which
|
|
are independent of the map's div, thus enabling us to link them to the map as needed.
|
|
When the user scrolls, the map is panned by the same amount as it would've been scrolled.
|
|
Also, some custom jquery scrollbars are used, to trigger the same scroll events across
|
|
browsers.
|
|
|
|
Z-index values:
|
|
-------------------------------------------
|
|
leaflet
|
|
-------------------------------------------
|
|
10 map
|
|
11 ruler
|
|
-------------------------------------------
|
|
under menu
|
|
-------------------------------------------
|
|
990 sidebar
|
|
999 toolbar-up
|
|
-------------------------------------------
|
|
menu items
|
|
-------------------------------------------
|
|
1000 main-menu(desktop-only), toolbar-down
|
|
1050 closebuttonwrapper (not being used currently)
|
|
-------------------------------------------
|
|
on the top
|
|
-------------------------------------------
|
|
1105 dialogs
|
|
1001 mobile-edit-button
|
|
1500 mobile-wizard (with class=menuwizard)
|
|
1501 toolbar-hamburger (with class=menuwizard-opened)
|
|
2000 vex-overlay
|
|
2001 vex dialog (vex-content)
|
|
-------------------------------------------
|