libreoffice-online/loleaflet
Ashod Nakashian d73167b734 bccu#1826 writer: jump to top when switch editor
Change-Id: I9d6f64d1e17f9dd5fb4e7d1b3ac8416be4623822
Reviewed-on: https://gerrit.libreoffice.org/25944
Reviewed-by: Ashod Nakashian <ashnakash@gmail.com>
Tested-by: Ashod Nakashian <ashnakash@gmail.com>
2016-06-05 23:58:01 +00:00
..
build loleaflet: bccu#1649 l10n of Admin Console 2016-05-31 23:31:16 +02:00
debug loleaflet: Consistency 2016-05-31 14:33:31 +05:30
dist loleaflet: enable l10n of Color picker widget 2016-06-03 12:25:54 +02:00
docs
node_modules
plugins/draw-0.2.4 tdf#94600: Do not reposition corner markers on 'dragend' 2016-01-27 10:10:15 +00:00
po loleaflet: updated translations 2016-05-28 23:13:35 +02:00
spec bccu#1699: Use 256 pixels at 96dpi as the TWIPs size (3840 TWIPs). 2016-04-18 14:42:25 +02:00
src bccu#1826 writer: jump to top when switch editor 2016-06-05 23:58:01 +00:00
util New loleaflet toolbar 2016-02-08 18:32:01 +01:00
.eslintignore loleaflet: Enable toolbar linting 2016-05-29 14:36:25 +05:30
.eslintrc loleaflet: Bump eslint version to 2.8.0 2016-05-29 14:59:52 +05:30
.gitattributes Treat minified files as binary files 2016-04-20 14:32:16 +05:30
.gitignore loleaflet: don't ignore errormessages.js 2016-05-23 23:53:08 +02:00
.npmignore
.travis.yml
CHANGELOG.md
evol.colorpicker.strings.js loleaflet: enable l10n of Color picker widget 2016-06-03 12:25:54 +02:00
FAQ.md
Jakefile.js loleaflet: Enable toolbar linting 2016-05-29 14:36:25 +05:30
LICENSE
Makefile loleaflet: enable l10n of Color picker widget 2016-06-03 12:25:54 +02:00
package.json loleaflet: Bump eslint version to 2.8.0 2016-05-29 14:59:52 +05:30
PLUGIN-GUIDE.md
README document --allowlocalstorage and --admincreds 2016-04-20 11:35:31 +02:00
reference.html Kill the rest of document_simple_example.html + related controls. 2016-05-19 00:43:51 +02:00
unocommands.js loleaflet: enable l10n of context menu items which come from src files from LO 2016-05-24 08:53:07 +02:00

Leaflet platform for LibreOffice On-Line
========================================

This is the client part of LibreOffice On-Line.  For the server part, see the
../loolwsd/README, and install it first.

Build dependencies
------------------

First you need to install 'jake'.  As root, do:

    npm install -g jake
    npm install

npm is provided by the nodejs package.
Alternatively, you can use the provided zip (as a normal user):

    unzip node_modules/modules.zip -d node_modules

A third 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

Building
--------

As a normal user:

    jake build
    cd plugins/draw-0.2.4
    jake build

Running
-------

To see an example:

* run loolwsd, like:

    ./loolwsd --systemplate=${SYSTEMPLATE} --lotemplate=${MASTER}/instdir --childroot=${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
fileserverroot manually for more control. See loolwsd/README for more information.

* open debug/document/document_simple_example.html or 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 file_path parameter is disabled by default for security reasons. You need to explicitly enable it
with the --allowlocalstorage 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.

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 --admincreds option of loolwsd. For example you can
pass --admincreds=admin/admin which will make username and password 'admin', 'admin' respectively. 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 tets 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 formated documentation. #
# See /loolwsd/reference.txt 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])
            + 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

    - 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('invalidatepreview', function (e) {})
            + e.id = the preview's id
        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'
        map.on('invalidatepreview', function (e) {})
            + e.id = the preview's id
        map.on('partpagerectangles', function (e) {}) where:
            + e.pixelRectangles = An array of bounds representing each page's dimension in pixels on the current zoom level
            + e.twipsRectangles = An array of bounds representing each page's dimension in twips.

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

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:

    * 'jake lint' should be run before commiting
    * 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.