{"__v":96,"_id":"54e6b5c0ba1a980d00e3ecb0","category":{"__v":7,"_id":"54d5635632d98b0d00384afd","pages":["54d5635632d98b0d00384b03","54d5635632d98b0d00384b04","54d5635632d98b0d00384b05","54d5635632d98b0d00384b06","54d5635632d98b0d00384b07","54d5635632d98b0d00384b08","54d5635632d98b0d00384b09","54d5635632d98b0d00384b0a","54d5635632d98b0d00384b0b","54d5635632d98b0d00384b0c","54d5635632d98b0d00384b0d","54d5635632d98b0d00384b0e","54d5635632d98b0d00384b0f","54e2254d22de1c230094b156","54e2255622de1c230094b158","54e23b349d045721004cbc26","54e65455d8873117005c773f","54e6b5c0ba1a980d00e3ecb0","551c12c781b8563500fd998e"],"project":"54d53c7b23010a0d001aca0c","version":"54d5635532d98b0d00384afb","sync":{"url":"","isSync":false},"reference":false,"createdAt":"2015-02-06T23:43:39.709Z","from_sync":false,"order":7,"slug":"plugin-base-classes","title":"Plugin Base Classes"},"parentDoc":null,"project":"54d53c7b23010a0d001aca0c","user":"54cfa8e1a8a4fd0d00b7fd1d","version":{"__v":10,"_id":"54d5635532d98b0d00384afb","forked_from":"54d53c7c23010a0d001aca0f","project":"54d53c7b23010a0d001aca0c","createdAt":"2015-02-07T00:59:01.934Z","releaseDate":"2015-02-07T00:59:01.934Z","categories":["54d5635632d98b0d00384afc","54d5635632d98b0d00384afd","54d5635632d98b0d00384afe","54d5635632d98b0d00384aff","54d5635632d98b0d00384b00","54d5635632d98b0d00384b01","54d5635632d98b0d00384b02","54d652097e05890d006f153e","54dd1315ca1e5219007e9daa","54e21e2b22de1c230094b147","54e68e62a43fe13500db3879","54fa1d3fe7a0ba2f00306309","551c453a23a1ee190034d19a","551df586e52a0b23000c62b6","551f39be6886f8230055f02a","55a6720751457325000e4d97"],"is_deprecated":false,"is_hidden":false,"is_beta":true,"is_stable":true,"codename":"","version_clean":"0.1.0","version":"0.1"},"updates":["55913d4081614f0d0039e4d0"],"next":{"pages":[],"description":""},"createdAt":"2015-02-20T04:19:12.778Z","link_external":false,"link_url":"","githubsync":"","sync_unique":"","hidden":false,"api":{"results":{"codes":[]},"auth":"required","params":[],"url":""},"isReference":false,"order":4,"body":"The preview pane is an [editor](doc:creating-an-editor-plugin) that hosts different types of `Previewers` that are responsible for rendering a certain type of content. The screenshot below shows the preview pane with the markdown previewer. Many previewers, such as the markdown previewer offer live previewing; they update the rendered content as the user makes changes in the source file. This article is aimed at writing a custom previewer.\n\nCloud9 comes with three default previewers:\n- *Raw* - displaying the file as is\n- *Browser* - display html/css files as well as any other file that a browser can display\n- *Markdown* - display a rendered version of a markdown file\n[block:image]\n{\n  \"images\": [\n    {\n      \"image\": [\n        \"https://files.readme.io/E8sEvNHqTyaf8StjMzmG_preview.png\",\n        \"preview.png\",\n        \"1428\",\n        \"1082\",\n        \"#608fba\",\n        \"\"\n      ]\n    }\n  ]\n}\n[/block]\n# Creating a Basic Previewer\n\nA basic previewer that is used by default for html files and http urls.\n\n```\nvar plugin = new Previewer(\"Your name\", main.consumes, {\n    caption: \"Name of Previewer\",\n    index: 10,\n    selector: function(path) {\n        return /(?:\\.html|\\.htm|\\.xhtml)$|^https?\\:\\/\\//.test(path);\n    }\n});\n```\n\nThis adds a menu item to the dropdown that contains a list of available previewers, which a user can manually select. Your previewer is automatically selected when the `selector` function returns truthy for a path of a file that is previewed.\n[block:image]\n{\n  \"images\": [\n    {\n      \"image\": [\n        \"https://files.readme.io/KifwTPHKSpGtPntPX5io_2015-03-24_1057.png\",\n        \"2015-03-24_1057.png\",\n        \"1360\",\n        \"350\",\n        \"#f8f8f8\",\n        \"\"\n      ]\n    }\n  ]\n}\n[/block]\n# Settings Menu\n\nThe settings menu, which shows when clicking on the settings button in the preview toolbar, can be configured by your plugin using the [context menu api](menus#context-menu). The `preview.settingsMenu` property gives a reference to the `Menu` object.\n\n```\nplugin.on(\"load\", function(){\n    preview.settingsMenu.append(new MenuItem({ \n        caption: \"Scroll Preview Element Into View\", \n        command: \"scrollPreviewElementIntoView\"\n    }, plugin));\n});\n```\n[block:image]\n{\n  \"images\": [\n    {\n      \"image\": [\n        \"https://files.readme.io/BFeJBLkSj2YSXtk8J6dA_Untitled-1.png\",\n        \"Untitled-1.png\",\n        \"312\",\n        \"163\",\n        \"#3c3c3c\",\n        \"\"\n      ]\n    }\n  ]\n}\n[/block]\n# Preview Sessions\n\nYour previewer plugin is a singleton that takes preview sessions and adds customized functionality to them, managing their lifetime from `start` to `end` and handles `activation` and `deactivation` of sessions as they are brought into view and out of view (via the context menu or tab switching).\n\nA preview session represents a preview of a single file. Use the session object to store data and objects related to that session. \n[block:image]\n{\n  \"images\": [\n    {\n      \"image\": [\n        \"https://files.readme.io/DgmGzU6QCHkOjm5B5lgD_Preview%20Sessions.png\",\n        \"Preview Sessions.png\",\n        \"577\",\n        \"499\",\n        \"\",\n        \"\"\n      ]\n    }\n  ]\n}\n[/block]\nIn the diagram above there are two panes and 6 tabs. Each of the four tabs with either a green or a red line is a preview to a different file (a session). The active tab (in white) shows the preview editor and the \"Draw Area\" is the area where your plugin can draw the rendered content of the file - more on this below. In this example there is one *inactive* tab that holds a markdown preview session. Both the *active* tabs and an additional *inactive* tab each hold a session from your previewer plugin.\n\n*See also the [guide on document sessions](documents#sessions) for more information on how sessions work in general.*\n\n## Start\n\nWhen a user opens a preview tab, for instance by using the preview button in the toolbar, a session is created and loaded into the appropriate previewer. The `sessionStart` event is then fired. Use this event to prepare the preview session to display rendered content in the draw area of the preview editor. It is only when the tab becomes active that the session will actually be displayed.\n\nThe following examples use an iframe element to render the preview of the contents of a file. This is by far the most common way of doing this. Lets start by creating an iframe element in the `sessionStart` handler.\n\n```\nplugin.on(\"sessionStart\", function(e) {\n    var session = e.session;\n    var editor = e.editor;\n    \n    var iframe = document.createElement(\"iframe\");\n    \n    iframe.style.width = \"100%\";\n    iframe.style.height = \"100%\";\n    iframe.style.border = 0;\n    \n    // Store objects on session\n    session.iframe = iframe;\n    session.editor = editor;\n    \n    editor.container.appendChild(session.iframe);\n});\n```\n\n### Loading already loaded session\n\nSessions are ended and started when a preview tab moves between panes. That is why it is highly recommended to only redo the part of the initialization that is specific to the preview editor.\n\n```\nplugin.on(\"sessionStart\", function(e) {\n    var session = e.session;\n    var editor = e.editor;\n    \n    if (session.iframe) {\n        session.editor = editor;\n        editor.container.appendChild(session.iframe);\n        return;\n    }\n```\n\n### Messaging between Iframe and previewer\n\nWe recommend using the [html5 postMessage() api](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to communicate between the iframe and your plugin. This api allows you to communicate even when the content of your iframe is from a different origin (port, protocol or (sub-)domain).\n\nAdd something like this to your `sessionStart` event handler to listen to commands from your iframe. The `previewOrigin` variable should be set to the origin string of whatever you load in your iframe.\n```\nsession.id = \"plugin-name\" + counter++;\n\nvar onMessage = function(e) {\n    if (c9.hosted && e.origin !== previewOrigin) // This is here for security reasons\n        return;\n    \n    if (e.data.id != session.id) // Only listen to messages for this session\n        return;\n    \n    if (e.data.message == \"stream.document\") {\n        // Send document here\n    }\n};\nwindow.addEventListener(\"message\", onMessage, false);\n```\n\nPreviewers can be opened for files that have a tab already open and for files that only exist on the file system. Loading the document's content and then sending it to the iframe can be done with the following code:\n\n```\nsession.source = e.source;\n\nif (session.previewTab) {\n    var doc = session.previewTab.document;\n    \n    session.source.postMessage({\n        type: \"document\",\n        content: doc.value\n    }, \"*\");\n    \n    if (!doc.hasValue())\n        doc.once(\"setValue\", function(){\n            emit(\"update\", { previewDocument: doc });\n        });\n}\nelse {\n    fs.readFile(session.path, function(err, data) {\n        if (err)\n            return showError(err.message);\n        \n        session.source.postMessage({\n            type: \"document\",\n            content: data\n        }, \"*\");\n    });\n}\n```\n\nIn the iframe you can send and receive messages like this. Note that in this example the `host` and `id` variables are send via the url.\n\n```\nvar host = location.href.match(/host=(.*?)(\\&|$)/)[1];\nvar id = location.href.match(/id=(.*)/)[1];\nvar parent = window.opener || window.parent;\n\nwindow.addEventListener(\"message\", function(e){\n    if (host != \"local\" && e.origin !== host) // This is here fore security reasons\n        return;\n    \n    if (e.data.type == \"document\") {\n        // Example\n        preview.innerHTML = renderContents(e.data.content);\n    }\n}, false);\n\nfunction send(message){\n    message.id = id;\n    parent.postMessage(message, host == \"local\" ? \"*\" : host);\n}\n\nsend({ message: \"stream.document\" });\n```\n\n### Destroying Session\n\nWhen a session is destroyed you want to clean up any references to objects and any events you might have set. Because the `sessionEnd` event is fired throughout the life time of a session you cannot use that to clean up these elements - your preview would lose state each time a tab is moved to a new pane.\n\nInstead add a `destroy()` method to the session.\n```\nsession.destroy = function(){\n    delete session.editor;\n    delete session.iframe;\n    \n    window.removeEventListener(\"message\", onMessage, false);\n}\n```\n\nThis is called when the tab that hosts the session is closed and all the popout windows are closed.\n\n## End\n\nThe `sessionEnd` event is fired when the tab that hosts the session is closed, when a user selects a different previewer and when the tab is moved to another pane. Use this event to clean up editor specific state.\n\n```\nplugin.on(\"sessionEnd\", function(e) {\n    var iframe = e.session.iframe;\n    \n    // Remove the iframe from the draw area\n    iframe.parentNode.removeChild(iframe);\n    \n    // Remove any residual loading state\n    e.tab.classList.remove(\"loading\");\n});\n```\n\n## Activate\n\nWhenever a preview tab becomes active, the session for that tab becomes active and the sessionActivate event is fired on your previewer plugin. A previewer plugin *must* implement this event to show the contents of the session that is now active.\n\n```\nplugin.on(\"sessionActivate\", function(e) {\n    var session = e.session;\n    \n    session.iframe.style.display = \"block\";\n    session.editor.setLocation(session.path);\n    session.editor.setButtonStyle(\"Markdown\", \"page_white.png\");\n});\n```\n\n## Deactivate\n\nWhenever a preview tab becomes inactive, the session for that tab becomes inactive and the sessionDeactivate event is fired on your previewer plugin. A previewer plugin *must* implement this event to hide the contents of the session that is no longer active.\n\n```\nplugin.on(\"sessionDeactivate\", function(e) {\n    var session = e.session;\n    session.iframe.style.display = \"none\";\n});\n```\n\n# Navigation\n\nWhen a preview is opened for the first time or any time when a user changes the path in the location bar of the preview editor, the `navigate` event is fired.\n\n```\nplugin.on(\"navigate\", function(e) {\n    var tab = e.tab;\n    var url = e.url;\n    var session = e.session;\n    var editor = session.editor;\n    var iframe = session.iframe;\n    \n    // Set the location of the iframe that will display the content\n    iframe.src = buildURL(url, session.id);\n    \n    // Set the state of the preview tab\n    tab.title = \n    tab.tooltip = \"[B] \" + url;\n    tab.classList.add(\"loading\");\n    \n    // Set the location of the location bar\n    editor.setLocation(url);\n});\n```\n\nThe `buildURL()` method used above can be anything. Make sure to add the `host` and `id` parameters used to set up communication between the iframe contents and this plugin if required.\n\n# Updating\n\nWhen a user changes a file that is being previewed, for instance by typing in an editor or by a file watcher that fires, the `update` event is fired.\n\nThe example below assumes the iframe messaging is set up and the full document contents is sent to that iframe when the update event is fired. For most common document sizes this is efficient to do at every key stroke.\n\n```\nplugin.on(\"update\", function(e) {\n    var session = e.session;\n    if (!session.source) return; // Renderer is not loaded yet\n\n    session.source.postMessage({\n        type: \"document\",\n        content: e.previewDocument.value\n    }, \"*\");\n});\n```\n\n# Reloading\n\nWhen the user hits the reload button or uses the `reloadpreview` command the `reload` event is fired to indicate that your plugin should refresh the rendered state.\n\nIn the example below the iframe reloads it's current location.\n```\nplugin.on(\"reload\", function(e){\n    var iframe = e.session.iframe;\n    iframe.src = iframe.src;\n});\n```\n\n# Popping Out into a new Window\n\nWhen a user clicks the pop out button in the preview toolbar the `popout` event is fired on your plugin. The user will expect a new window (or tab) to open with the preview content shown. This is especially useful when the user wants to use the built-in browser tools or share the preview with another person.\n\nIn this example the iframe source is cleaned up and used to open a new window.\n```\nplugin.on(\"popout\", function(e){\n    var src = getIframeSrc(e.session.iframe);\n    window.open(src);\n});\n```\n\n# Serializing and Deserializing\n\nSimilar to [how editors serialize and deserialize state](http://cloud9-sdk.readme.io/v0.1/docs/creating-an-editor-plugin#serializing-and-deserializing), use the `getState` and `setState` events to get and set any additional state you'd like to store between reloads of the IDE.\n```\nplugin.on(\"getState\", function(e) {\n    var session = e.session;\n    var state = e.state;\n    \n    state.myProperty = session.myProperty;\n});\n\nplugin.on(\"setState\", function(e) {\n    var session = e.session;\n    var state = e.state;\n    \n    session.myProperty = state.myProperty;\n});\n```\n\n# Full Example\n\nThe example below is the actual implementation of the [markdown previewer](https://github.com/c9/c9.ide.preview.markdown).\n```\ndefine(function(require, exports, module) {\n    main.consumes = [\n        \"c9\", \"Previewer\", \"fs\", \"dialog.error\", \"commands\", \"tabManager\", \n        \"layout\", \"settings\"\n    ];\n    main.provides = [\"preview.markdown\"];\n    return main;\n\n    function main(options, imports, register) {\n        var Previewer = imports.Previewer;\n        var c9 = imports.c9;\n        var fs = imports.fs;\n        var commands = imports.commands;\n        var layout = imports.layout;\n        var settings = imports.settings;\n        var tabManager = imports.tabManager;\n        var showError = imports[\"dialog.error\"].show;\n        var dirname = require(\"path\").dirname;\n        \n        /***** Initialization *****/\n        \n        var plugin = new Previewer(\"Ajax.org\", main.consumes, {\n            caption: \"Markdown\",\n            index: 200,\n            selector: function(path) {\n                return path && path.match(/(?:\\.md|\\.markdown)$/i);\n            }\n        });\n        var emit = plugin.getEmitter();\n        \n        var BGCOLOR = { \n            \"flat-light\": \"rgb(250,250,250)\", \n            \"light\": \"rgba(255, 255, 255, 0.88)\", \n            \"light-gray\": \"rgba(255, 255, 255, 0.88)\",\n            \"dark\": \"rgba(255, 255, 255, 0.88)\",\n            \"dark-gray\": \"rgba(255, 255, 255, 0.88)\" \n        };\n        \n        var counter = 0;\n        var HTMLURL, previewOrigin;\n        \n        /***** Methods *****/\n        \n        function getPreviewUrl(fn) {\n            if (options.local && document.baseURI.substr(0, 5) == \"file:\")\n                return setTimeout(getPreviewUrl.bind(null, fn), 100);\n            else if (HTMLURL)\n                return fn(HTMLURL);\n            \n            HTMLURL = (options.staticPrefix \n                ? (options.local ? dirname(document.baseURI) + \"/static\" : options.staticPrefix) \n                    + options.htmlPath\n                : \"/static/plugins/c9.ide.preview/previewers/markdown.html\")\n                    + \"?host=\" + (options.local ? \"local\" : location.origin);\n                \n            if (HTMLURL.charAt(0) == \"/\")\n                HTMLURL = location.protocol + \"//\" + location.host + HTMLURL;\n    \n            previewOrigin = HTMLURL.match(/^(?:[^\\/]|\\/\\/)*/)[0];\n            \n            fn(HTMLURL);\n        }\n        \n        /***** Lifecycle *****/\n        \n        plugin.on(\"load\", function(){\n            \n        });\n        plugin.on(\"sessionStart\", function(e) {\n            var doc = e.doc;\n            var session = e.session;\n            var tab = e.tab;\n            var editor = e.editor;\n            \n            if (session.iframe) {\n                session.editor = editor;\n                editor.container.appendChild(session.iframe);\n                return;\n            }\n            \n            var iframe = document.createElement(\"iframe\");\n            \n            iframe.setAttribute(\"nwfaketop\", true);\n            iframe.setAttribute(\"nwdisable\", true);\n\n            iframe.style.width = \"100%\";\n            iframe.style.height = \"100%\";\n            iframe.style.border = 0;\n            \n            function setTheme(e) {\n                iframe.style.backgroundColor = BGCOLOR[e.theme];\n            }\n            layout.on(\"themeChange\", setTheme, doc);\n            setTheme({ theme: settings.get(\"user/general/:::at:::skin\") });\n            \n            if (options.local) {\n                iframe.addEventListener(\"load\", function(){\n                    // @todo to do this correctly stack needs to allow switching previewer\n                    // plugin.activeSession.add(iframe.contentWindow.location.href);\n                    \n                    iframe.contentWindow.opener = window;\n                    if (iframe.contentWindow.start)\n                        iframe.contentWindow.start(window);\n                });\n            }\n            \n            var onMessage = function(e) {\n                if (c9.hosted && e.origin !== previewOrigin)\n                    return;\n                \n                if (e.data.id != session.id)\n                    return;\n                \n                if (e.data.message == \"exec\") {\n                    commands.exec(e.data.command);\n                }\n                else if (e.data.message == \"focus\") {\n                    tabManager.focusTab(tab);\n                }\n                else if (e.data.message == \"stream.document\") {\n                    session.source = e.source;\n                    \n                    if (session.previewTab) {\n                        var doc = session.previewTab.document;\n                        \n                        session.source.postMessage({\n                            type: \"document\",\n                            content: doc.value\n                        }, \"*\");\n                        \n                        if (!doc.hasValue())\n                            doc.once(\"setValue\", function(){\n                                emit(\"update\", { previewDocument: doc });\n                            });\n                    }\n                    else {\n                        fs.readFile(session.path, function(err, data) {\n                            if (err)\n                                return showError(err.message);\n                            \n                            session.source.postMessage({\n                                type: \"document\",\n                                content: data\n                            }, \"*\");\n                        });\n                    }\n                    \n                    session.source.postMessage({\n                        type: \"keys\",\n                        keys: commands.getExceptionBindings()\n                    }, \"*\");\n                    \n                    tab.classList.remove(\"loading\");\n                }\n            };\n            window.addEventListener(\"message\", onMessage, false);\n            \n            // Set iframe\n            session.iframe = iframe;\n            session.id = \"markdown\" + counter++;\n            \n            session.destroy = function(){\n                delete session.editor;\n                delete session.iframe;\n                \n                window.removeEventListener(\"message\", onMessage, false);\n            }\n            \n            // Load the markup renderer\n            getPreviewUrl(function(url) { \n                iframe.src = url + \"&id=\" + session.id; \n            });\n            \n            session.editor = editor;\n            editor.container.appendChild(session.iframe);\n        });\n        plugin.on(\"sessionEnd\", function(e) {\n            var tab = e.tab;\n            var session = e.session;\n            var iframe = session.iframe;\n            \n            iframe.parentNode.removeChild(iframe);\n            \n            tab.classList.remove(\"loading\");\n        });\n        plugin.on(\"sessionActivate\", function(e) {\n            var session = e.session;\n            \n            session.iframe.style.display = \"block\";\n            session.editor.setLocation(session.path);\n            session.editor.setButtonStyle(\"Markdown\", \"page_white.png\");\n        });\n        plugin.on(\"sessionDeactivate\", function(e) {\n            var session = e.session;\n            session.iframe.style.display = \"none\";\n        });\n        plugin.on(\"navigate\", function(e) {\n            var tab = e.tab;\n            var session = e.session;\n            var iframe = session.iframe;\n            var editor = session.editor;\n            \n            tab.classList.add(\"loading\");\n            \n            tab.title = \n            tab.tooltip = \"[M] \" + e.url;\n            editor.setLocation(e.url);\n            \n            iframe.src = iframe.src;\n        });\n        plugin.on(\"update\", function(e) {\n            var session = e.session;\n            if (!session.source) return; // Renderer is not loaded yet\n    \n            session.source.postMessage({\n                type: \"document\",\n                content: e.previewDocument.value\n            }, \"*\");\n        });\n        plugin.on(\"reload\", function(e){\n            plugin.activeDocument.tab.classList.add(\"loading\");\n            \n            var iframe = e.session.iframe;\n            iframe.src = iframe.src;\n        });\n        plugin.on(\"popout\", function(e){\n            var src = e.session.iframe.src;\n            window.open(src);\n        });\n        plugin.on(\"unload\", function(){\n        });\n        \n        /***** Register and define API *****/\n        \n        /**\n         * Previewer for markdown content.\n         **/\n        plugin.freezePublicAPI({\n        });\n        \n        register(null, {\n            \"preview.markdown\": plugin\n        });\n    }\n});\n```","excerpt":"","slug":"previewer","type":"basic","title":"Previewer Plugin"}
The preview pane is an [editor](doc:creating-an-editor-plugin) that hosts different types of `Previewers` that are responsible for rendering a certain type of content. The screenshot below shows the preview pane with the markdown previewer. Many previewers, such as the markdown previewer offer live previewing; they update the rendered content as the user makes changes in the source file. This article is aimed at writing a custom previewer. Cloud9 comes with three default previewers: - *Raw* - displaying the file as is - *Browser* - display html/css files as well as any other file that a browser can display - *Markdown* - display a rendered version of a markdown file [block:image] { "images": [ { "image": [ "https://files.readme.io/E8sEvNHqTyaf8StjMzmG_preview.png", "preview.png", "1428", "1082", "#608fba", "" ] } ] } [/block] # Creating a Basic Previewer A basic previewer that is used by default for html files and http urls. ``` var plugin = new Previewer("Your name", main.consumes, { caption: "Name of Previewer", index: 10, selector: function(path) { return /(?:\.html|\.htm|\.xhtml)$|^https?\:\/\//.test(path); } }); ``` This adds a menu item to the dropdown that contains a list of available previewers, which a user can manually select. Your previewer is automatically selected when the `selector` function returns truthy for a path of a file that is previewed. [block:image] { "images": [ { "image": [ "https://files.readme.io/KifwTPHKSpGtPntPX5io_2015-03-24_1057.png", "2015-03-24_1057.png", "1360", "350", "#f8f8f8", "" ] } ] } [/block] # Settings Menu The settings menu, which shows when clicking on the settings button in the preview toolbar, can be configured by your plugin using the [context menu api](menus#context-menu). The `preview.settingsMenu` property gives a reference to the `Menu` object. ``` plugin.on("load", function(){ preview.settingsMenu.append(new MenuItem({ caption: "Scroll Preview Element Into View", command: "scrollPreviewElementIntoView" }, plugin)); }); ``` [block:image] { "images": [ { "image": [ "https://files.readme.io/BFeJBLkSj2YSXtk8J6dA_Untitled-1.png", "Untitled-1.png", "312", "163", "#3c3c3c", "" ] } ] } [/block] # Preview Sessions Your previewer plugin is a singleton that takes preview sessions and adds customized functionality to them, managing their lifetime from `start` to `end` and handles `activation` and `deactivation` of sessions as they are brought into view and out of view (via the context menu or tab switching). A preview session represents a preview of a single file. Use the session object to store data and objects related to that session. [block:image] { "images": [ { "image": [ "https://files.readme.io/DgmGzU6QCHkOjm5B5lgD_Preview%20Sessions.png", "Preview Sessions.png", "577", "499", "", "" ] } ] } [/block] In the diagram above there are two panes and 6 tabs. Each of the four tabs with either a green or a red line is a preview to a different file (a session). The active tab (in white) shows the preview editor and the "Draw Area" is the area where your plugin can draw the rendered content of the file - more on this below. In this example there is one *inactive* tab that holds a markdown preview session. Both the *active* tabs and an additional *inactive* tab each hold a session from your previewer plugin. *See also the [guide on document sessions](documents#sessions) for more information on how sessions work in general.* ## Start When a user opens a preview tab, for instance by using the preview button in the toolbar, a session is created and loaded into the appropriate previewer. The `sessionStart` event is then fired. Use this event to prepare the preview session to display rendered content in the draw area of the preview editor. It is only when the tab becomes active that the session will actually be displayed. The following examples use an iframe element to render the preview of the contents of a file. This is by far the most common way of doing this. Lets start by creating an iframe element in the `sessionStart` handler. ``` plugin.on("sessionStart", function(e) { var session = e.session; var editor = e.editor; var iframe = document.createElement("iframe"); iframe.style.width = "100%"; iframe.style.height = "100%"; iframe.style.border = 0; // Store objects on session session.iframe = iframe; session.editor = editor; editor.container.appendChild(session.iframe); }); ``` ### Loading already loaded session Sessions are ended and started when a preview tab moves between panes. That is why it is highly recommended to only redo the part of the initialization that is specific to the preview editor. ``` plugin.on("sessionStart", function(e) { var session = e.session; var editor = e.editor; if (session.iframe) { session.editor = editor; editor.container.appendChild(session.iframe); return; } ``` ### Messaging between Iframe and previewer We recommend using the [html5 postMessage() api](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) to communicate between the iframe and your plugin. This api allows you to communicate even when the content of your iframe is from a different origin (port, protocol or (sub-)domain). Add something like this to your `sessionStart` event handler to listen to commands from your iframe. The `previewOrigin` variable should be set to the origin string of whatever you load in your iframe. ``` session.id = "plugin-name" + counter++; var onMessage = function(e) { if (c9.hosted && e.origin !== previewOrigin) // This is here for security reasons return; if (e.data.id != session.id) // Only listen to messages for this session return; if (e.data.message == "stream.document") { // Send document here } }; window.addEventListener("message", onMessage, false); ``` Previewers can be opened for files that have a tab already open and for files that only exist on the file system. Loading the document's content and then sending it to the iframe can be done with the following code: ``` session.source = e.source; if (session.previewTab) { var doc = session.previewTab.document; session.source.postMessage({ type: "document", content: doc.value }, "*"); if (!doc.hasValue()) doc.once("setValue", function(){ emit("update", { previewDocument: doc }); }); } else { fs.readFile(session.path, function(err, data) { if (err) return showError(err.message); session.source.postMessage({ type: "document", content: data }, "*"); }); } ``` In the iframe you can send and receive messages like this. Note that in this example the `host` and `id` variables are send via the url. ``` var host = location.href.match(/host=(.*?)(\&|$)/)[1]; var id = location.href.match(/id=(.*)/)[1]; var parent = window.opener || window.parent; window.addEventListener("message", function(e){ if (host != "local" && e.origin !== host) // This is here fore security reasons return; if (e.data.type == "document") { // Example preview.innerHTML = renderContents(e.data.content); } }, false); function send(message){ message.id = id; parent.postMessage(message, host == "local" ? "*" : host); } send({ message: "stream.document" }); ``` ### Destroying Session When a session is destroyed you want to clean up any references to objects and any events you might have set. Because the `sessionEnd` event is fired throughout the life time of a session you cannot use that to clean up these elements - your preview would lose state each time a tab is moved to a new pane. Instead add a `destroy()` method to the session. ``` session.destroy = function(){ delete session.editor; delete session.iframe; window.removeEventListener("message", onMessage, false); } ``` This is called when the tab that hosts the session is closed and all the popout windows are closed. ## End The `sessionEnd` event is fired when the tab that hosts the session is closed, when a user selects a different previewer and when the tab is moved to another pane. Use this event to clean up editor specific state. ``` plugin.on("sessionEnd", function(e) { var iframe = e.session.iframe; // Remove the iframe from the draw area iframe.parentNode.removeChild(iframe); // Remove any residual loading state e.tab.classList.remove("loading"); }); ``` ## Activate Whenever a preview tab becomes active, the session for that tab becomes active and the sessionActivate event is fired on your previewer plugin. A previewer plugin *must* implement this event to show the contents of the session that is now active. ``` plugin.on("sessionActivate", function(e) { var session = e.session; session.iframe.style.display = "block"; session.editor.setLocation(session.path); session.editor.setButtonStyle("Markdown", "page_white.png"); }); ``` ## Deactivate Whenever a preview tab becomes inactive, the session for that tab becomes inactive and the sessionDeactivate event is fired on your previewer plugin. A previewer plugin *must* implement this event to hide the contents of the session that is no longer active. ``` plugin.on("sessionDeactivate", function(e) { var session = e.session; session.iframe.style.display = "none"; }); ``` # Navigation When a preview is opened for the first time or any time when a user changes the path in the location bar of the preview editor, the `navigate` event is fired. ``` plugin.on("navigate", function(e) { var tab = e.tab; var url = e.url; var session = e.session; var editor = session.editor; var iframe = session.iframe; // Set the location of the iframe that will display the content iframe.src = buildURL(url, session.id); // Set the state of the preview tab tab.title = tab.tooltip = "[B] " + url; tab.classList.add("loading"); // Set the location of the location bar editor.setLocation(url); }); ``` The `buildURL()` method used above can be anything. Make sure to add the `host` and `id` parameters used to set up communication between the iframe contents and this plugin if required. # Updating When a user changes a file that is being previewed, for instance by typing in an editor or by a file watcher that fires, the `update` event is fired. The example below assumes the iframe messaging is set up and the full document contents is sent to that iframe when the update event is fired. For most common document sizes this is efficient to do at every key stroke. ``` plugin.on("update", function(e) { var session = e.session; if (!session.source) return; // Renderer is not loaded yet session.source.postMessage({ type: "document", content: e.previewDocument.value }, "*"); }); ``` # Reloading When the user hits the reload button or uses the `reloadpreview` command the `reload` event is fired to indicate that your plugin should refresh the rendered state. In the example below the iframe reloads it's current location. ``` plugin.on("reload", function(e){ var iframe = e.session.iframe; iframe.src = iframe.src; }); ``` # Popping Out into a new Window When a user clicks the pop out button in the preview toolbar the `popout` event is fired on your plugin. The user will expect a new window (or tab) to open with the preview content shown. This is especially useful when the user wants to use the built-in browser tools or share the preview with another person. In this example the iframe source is cleaned up and used to open a new window. ``` plugin.on("popout", function(e){ var src = getIframeSrc(e.session.iframe); window.open(src); }); ``` # Serializing and Deserializing Similar to [how editors serialize and deserialize state](http://cloud9-sdk.readme.io/v0.1/docs/creating-an-editor-plugin#serializing-and-deserializing), use the `getState` and `setState` events to get and set any additional state you'd like to store between reloads of the IDE. ``` plugin.on("getState", function(e) { var session = e.session; var state = e.state; state.myProperty = session.myProperty; }); plugin.on("setState", function(e) { var session = e.session; var state = e.state; session.myProperty = state.myProperty; }); ``` # Full Example The example below is the actual implementation of the [markdown previewer](https://github.com/c9/c9.ide.preview.markdown). ``` define(function(require, exports, module) { main.consumes = [ "c9", "Previewer", "fs", "dialog.error", "commands", "tabManager", "layout", "settings" ]; main.provides = ["preview.markdown"]; return main; function main(options, imports, register) { var Previewer = imports.Previewer; var c9 = imports.c9; var fs = imports.fs; var commands = imports.commands; var layout = imports.layout; var settings = imports.settings; var tabManager = imports.tabManager; var showError = imports["dialog.error"].show; var dirname = require("path").dirname; /***** Initialization *****/ var plugin = new Previewer("Ajax.org", main.consumes, { caption: "Markdown", index: 200, selector: function(path) { return path && path.match(/(?:\.md|\.markdown)$/i); } }); var emit = plugin.getEmitter(); var BGCOLOR = { "flat-light": "rgb(250,250,250)", "light": "rgba(255, 255, 255, 0.88)", "light-gray": "rgba(255, 255, 255, 0.88)", "dark": "rgba(255, 255, 255, 0.88)", "dark-gray": "rgba(255, 255, 255, 0.88)" }; var counter = 0; var HTMLURL, previewOrigin; /***** Methods *****/ function getPreviewUrl(fn) { if (options.local && document.baseURI.substr(0, 5) == "file:") return setTimeout(getPreviewUrl.bind(null, fn), 100); else if (HTMLURL) return fn(HTMLURL); HTMLURL = (options.staticPrefix ? (options.local ? dirname(document.baseURI) + "/static" : options.staticPrefix) + options.htmlPath : "/static/plugins/c9.ide.preview/previewers/markdown.html") + "?host=" + (options.local ? "local" : location.origin); if (HTMLURL.charAt(0) == "/") HTMLURL = location.protocol + "//" + location.host + HTMLURL; previewOrigin = HTMLURL.match(/^(?:[^\/]|\/\/)*/)[0]; fn(HTMLURL); } /***** Lifecycle *****/ plugin.on("load", function(){ }); plugin.on("sessionStart", function(e) { var doc = e.doc; var session = e.session; var tab = e.tab; var editor = e.editor; if (session.iframe) { session.editor = editor; editor.container.appendChild(session.iframe); return; } var iframe = document.createElement("iframe"); iframe.setAttribute("nwfaketop", true); iframe.setAttribute("nwdisable", true); iframe.style.width = "100%"; iframe.style.height = "100%"; iframe.style.border = 0; function setTheme(e) { iframe.style.backgroundColor = BGCOLOR[e.theme]; } layout.on("themeChange", setTheme, doc); setTheme({ theme: settings.get("user/general/@skin") }); if (options.local) { iframe.addEventListener("load", function(){ // @todo to do this correctly stack needs to allow switching previewer // plugin.activeSession.add(iframe.contentWindow.location.href); iframe.contentWindow.opener = window; if (iframe.contentWindow.start) iframe.contentWindow.start(window); }); } var onMessage = function(e) { if (c9.hosted && e.origin !== previewOrigin) return; if (e.data.id != session.id) return; if (e.data.message == "exec") { commands.exec(e.data.command); } else if (e.data.message == "focus") { tabManager.focusTab(tab); } else if (e.data.message == "stream.document") { session.source = e.source; if (session.previewTab) { var doc = session.previewTab.document; session.source.postMessage({ type: "document", content: doc.value }, "*"); if (!doc.hasValue()) doc.once("setValue", function(){ emit("update", { previewDocument: doc }); }); } else { fs.readFile(session.path, function(err, data) { if (err) return showError(err.message); session.source.postMessage({ type: "document", content: data }, "*"); }); } session.source.postMessage({ type: "keys", keys: commands.getExceptionBindings() }, "*"); tab.classList.remove("loading"); } }; window.addEventListener("message", onMessage, false); // Set iframe session.iframe = iframe; session.id = "markdown" + counter++; session.destroy = function(){ delete session.editor; delete session.iframe; window.removeEventListener("message", onMessage, false); } // Load the markup renderer getPreviewUrl(function(url) { iframe.src = url + "&id=" + session.id; }); session.editor = editor; editor.container.appendChild(session.iframe); }); plugin.on("sessionEnd", function(e) { var tab = e.tab; var session = e.session; var iframe = session.iframe; iframe.parentNode.removeChild(iframe); tab.classList.remove("loading"); }); plugin.on("sessionActivate", function(e) { var session = e.session; session.iframe.style.display = "block"; session.editor.setLocation(session.path); session.editor.setButtonStyle("Markdown", "page_white.png"); }); plugin.on("sessionDeactivate", function(e) { var session = e.session; session.iframe.style.display = "none"; }); plugin.on("navigate", function(e) { var tab = e.tab; var session = e.session; var iframe = session.iframe; var editor = session.editor; tab.classList.add("loading"); tab.title = tab.tooltip = "[M] " + e.url; editor.setLocation(e.url); iframe.src = iframe.src; }); plugin.on("update", function(e) { var session = e.session; if (!session.source) return; // Renderer is not loaded yet session.source.postMessage({ type: "document", content: e.previewDocument.value }, "*"); }); plugin.on("reload", function(e){ plugin.activeDocument.tab.classList.add("loading"); var iframe = e.session.iframe; iframe.src = iframe.src; }); plugin.on("popout", function(e){ var src = e.session.iframe.src; window.open(src); }); plugin.on("unload", function(){ }); /***** Register and define API *****/ /** * Previewer for markdown content. **/ plugin.freezePublicAPI({ }); register(null, { "preview.markdown": plugin }); } }); ```