UNPKG

@phoenix-plugin-registry/brackets-markdown-preview

Version:

Markdown live preview incl. detached window, code syntax highlighting, output themes, adaptive preview width, graphical checkboxes, activation on start...

448 lines (400 loc) 19.5 kB
/** * @module ExtPreviewPanel * @file Functions and classes handling with the panel in charge of displaying the (HTML) preview * @author Loïs Bégué * @license MIT license (MIT) * @copyright Copyright (c) 2017 Loïs Bégué */ /*global define, $, brackets, window */ define(function (require, exports, module) { "use strict"; // Brackets modules var WorkspaceManager = brackets.getModule("view/WorkspaceManager"); var PopUpManager = brackets.getModule("widgets/PopUpManager"); var EditorManager = brackets.getModule("editor/EditorManager"); // Other modules var ExtUtils = require("scripts/ExtUtils"); // Variables for this module var _previewPanel = null; var _previewSpinner = null; var _previewVisible = false; var _previewContentVisible = false; var _detachedPreview = null; var _PreviewSettingsDialog = null; var _PreviewSettingsIcon = null; var _onLoadPreferencesValues = null; /******************************************** * Helper functions ********************************************/ /** * Some global event handlers should be registered when the extension is initialized. * These handlers are in charge of resizing and hiding/showing the preview panel. * @private * @author Loïs Bégué */ function _registerPermanentEventHandlers(){ // Register the event handler to allow "ESC" key to close the setting dialog. // The parameter "autoRemove" is set to false so the dialog is NOT removed from DOM. PopUpManager.addPopUp(_PreviewSettingsDialog, _hideSettingsDialog, false); // Register an handler for the "panelResizeUpdate" event of the preview panel. // As the preview is a brackets "bottom" panel, the panelResizeUpdate is // fired when the "height" of the panel has been modified e.g. by the user... _previewPanel.panel.on("panelResizeUpdate", _resizeiFrameHeight); // register event handler for some brackets "global" events WorkspaceManager.on("workspaceUpdateLayout", _resizeiFrameWidthGlobal); $("#sidebar").on("panelCollapsed panelExpanded panelResizeUpdate", _resizeiFrameWidth); } /******************************************** * SPINNER show/hide ********************************************/ /** * Shows a spinner symbol to make the user aware of an ongoing operation that make take some time. * @author Loïs Bégué */ function showSpinner() { _previewSpinner.show(); } /** * Hides a previously displayed spinner symbol that makes the user aware of an ongoing operation. * @author Loïs Bégué */ function hideSpinner() { _previewSpinner.hide(); } /******************************************** * PANEL show/hide/Resize ********************************************/ /** * The preview panel is hidden when this function is called. * @author Loïs Bégué */ function hidePreviewPanel() { if (!_previewVisible){ return; } _previewVisible = false; _previewPanel.hide(); if (_detachedPreview && !_detachedPreview.closed) { _detachedPreview.close(); } } /** * The preview panel is made visible when this function is called. * @author Loïs Bégué */ function showPreviewPanel() { if (_previewVisible){ return; } _previewVisible = true; _previewPanel.show(); } /******************************************** * CONTENT iFrame show/hide/resize/... ********************************************/ /** * The content within the preview panel is hidden/deactivated when this function is called. * @author Loïs Bégué */ function hidePreviewContent() { _previewContentVisible = false; _previewPanel.iFrame[0].contentDocument.body.removeEventListener("click", _frameLinkClickedEvent, true); _previewPanel.iFrame.hide(); } /** * The content within the preview panel is made visible when this function is called. The content is then able to react to click events. * @author Loïs Bégué */ function showPreviewContent(){ _previewContentVisible = true; _previewPanel.iFrame[0].contentDocument.body.addEventListener("click", _frameLinkClickedEvent, true); _resizeiFrameWidth(); _previewPanel.iFrame.attr("height", _previewPanel.panel.height()); _previewPanel.iFrame.show(); } /** * Gives the visibility status of the preview panel back * @author Loïs Bégué * @returns {boolean} TRUE when the preview panel is visible i.e. when its attributes are set accordingly. */ function isPanelVisible(){ return _previewVisible; } /** * Gives the visibility status of the content within the preview panel. * @author Loïs Bégué * @returns {boolean} TRUE when the content is visible within the preview panel i.e. when its attributes are set accordingly. */ function isContentVisible(){ return _previewContentVisible; } /** * Set the width of the Preview Content iFrame (by setting the corresponding attribute). * @private */ function _setPreviewContentWidth(){ let newWidth = _previewPanel.panel.innerWidth() + "px"; // check if the width was modified or not if (newWidth !== _previewPanel.iFrame.attr("width")){ _previewPanel.iFrame.attr("width", newWidth); } } /** * The width of the preview iFrame (if panel visible) is calculated and set according to the size of its container/parent panel. This handler will be fired when the panel is collapsed, expanded or resized. * This function is also used as event handler to catch some Brackets resizing events. * For extension internal usage only. * @private * @author Loïs Bégué */ function _resizeiFrameWidth(){ // if the preview isn't there, then we have nothing to do if (!_previewVisible || !_previewContentVisible){ return; } // set the correct width in the iFrame attribute _setPreviewContentWidth(); } /** * The width of the preview iFrame (if panel visible) is calculated and set according to the size of its container/parent panel. This handler will be fired when the brackets workspace/windows is resized. * For extension internal usage only. * @private * @author Loïs Bégué */ function _resizeiFrameWidthGlobal(workspaceUpdateLayoutEvent, availableWorkspaceSize){ // The event "workspaceUpdateLayout" is fired twice in a row - at least // on windows platform - when the brackets window is restored from the task bar. // The first time, the available workspace size is 0: // In that case, we have nothing to do. if (availableWorkspaceSize<1) { return; } // if the preview isn't there, then we have nothing to do if (!_previewVisible || !_previewContentVisible){ return; } // set the correct width in the iFrame attribute _setPreviewContentWidth(); } /** * The height of the preview iFrame is calculated and set according to the size of its container/parent panel. * This function is an event handler to catch the resizing events of the Brackets panel containing the preview. * For extension internal usage only. * @private * @author Loïs Bégué */ function _resizeiFrameHeight(resizePanelEvent, newPanelHeight){ _previewPanel.iFrame.attr("height", newPanelHeight); } /** * When the scroll position within the currently editet document and the preview content should be synchronized, then * the scrolling position of the preview sometimes needs to be calculated. * This function compute the position accordingly. * @private * @author Loïs Bégué * @returns {number} The scrolling position of the preview according to the scrolling position within the corresponding edited document. */ function _calcScrollPosition(){ let result = 0; if ("scrollHeight" in _previewPanel.iFrame[0].contentDocument.body) { let currentEditor = EditorManager.getCurrentFullEditor(); let scrollInfo = currentEditor._codeMirror.getScrollInfo(); let scrollPercentage = scrollInfo.top / (scrollInfo.height - scrollInfo.clientHeight); let currentScrollHeigth = _previewPanel.iFrame[0].contentDocument.body.scrollHeight; result = Math.round((currentScrollHeigth - _previewPanel.iFrame[0].clientHeight) * scrollPercentage); } return result; } /** * The scrolling position of the content is set to the given input scrolling position of the editor. * This function is mainly used as an event handler. * @author Loïs Bégué * @param {number} newScrollPosition The scrolling position of within the edited document. */ function scroll(newScrollPosition){ if (!_previewPanel.isContentVisible() || !_previewPanel.iFrame[0].contentDocument.body) { return; } // if an internal anchor has been clicked (a location hash has been set), then we need // to reset the hash of the url location before scrolling or setting (user clicking a // bookmark link) a new location hash. _previewPanel.iFrame[0].contentDocument.location.hash = ""; // now set/calculate the scroll position _previewPanel.iFrame[0].contentDocument.body.scrollTop = newScrollPosition? newScrollPosition : _calcScrollPosition(); } /** * The preview content (within the iFrame body element) is replaced by the input string. The iFrame doesn't reinitilize itself during this operation. * @author Loïs Bégué * @param {string} HTMLContent The content i.e. source code of the html preview document to be displayed within the preview panel. */ function setHTMLContent(HTMLContent){ _previewPanel.iFrame[0].contentDocument.body.innerHTML = HTMLContent; _updateDetachedPreviewContent(HTMLContent); } /** * The source of the preview (iFrame source document) is replaced by the input string. Setting the source will enforce the iFrame to reload/reinitialize. * @author Loïs Bégué * @param {string} HTMLSource The content i.e. source code of the html preview document to be displayed within the preview panel. * @param {function} onLoadCallback This callback will be called once the iFrame has been reloaded. */ function setHTMLSource(HTMLSource, onLoadCallback){ _previewPanel.iFrame.attr("srcdoc", HTMLSource); _previewPanel.iFrame.off("load"); _previewPanel.iFrame.load(onLoadCallback); _updateDetachedPreviewSource(HTMLSource); } function _updateDetachedPreviewSource(documentSource) { if (!_detachedPreview || _detachedPreview.closed) { return; } _detachedPreview.document.open(); _detachedPreview.document.write(documentSource); _detachedPreview.document.close(); } function _updateDetachedPreviewContent(documentContent) { if (!_detachedPreview || _detachedPreview.closed) { return; } _detachedPreview.document.body.innerHTML = documentContent; } /** * This function opens a supplemental window (if not already there) and * load the already generated preview as its content * @author Loïs Bégué */ function showDetachedPreview(){ if (!_detachedPreview || _detachedPreview.closed) { _detachedPreview = window.open(""); _detachedPreview.moveTo(100, 100); } _updateDetachedPreviewSource(_previewPanel.iFrame.attr("srcdoc")); _detachedPreview.focus(); } /******************************************** * SETTINGS related functions... ********************************************/ /** * This function is an event handler responsible of closing/hiding the Settings dialog. * The event is fired by a click within the preview PANEL. * @private * @author Loïs Bégué * @param {object} clickEvent The event object that initiated the "click" event. */ function _documentClickToCloseSettingsEvent(clickEvent) { if (_PreviewSettingsDialog.isVisible() && !_isSettingsClicked(clickEvent)) { _hideSettingsDialog(); } } /** * This function is an event handler responsible of closing/hiding the Settings dialog. * The event is fired by a click within the IFRAME of the preview panel. * @private * @author Loïs Bégué * @param {object} clickEvent The event object that initiated the "click" event. */ function _frameContentClickedEvent(clickEvent) { _hideSettingsDialog(); } /** * This function is an event handler responsible of opening URL from LINKS clicked within the iFrame of the preview panel. * The event is fired by a click a link within the iFrame of the preview panel. * @private * @author Loïs Bégué * @param {object} clickEvent The event object that initiated the "click" event. */ function _frameLinkClickedEvent(clickEvent) { // Open external browser when links are clicked ExtUtils.openAnchorURL(clickEvent.target, () => clickEvent.preventDefault() ); } /** * This function is an event handler responsible of opening and closing the setting dialog when the "gear" icon is clicked. * @private * @author Loïs Bégué * @param {object} clickEvent The event object that initiated the "click" event. */ function _settingsIconClickedEvent(clickEvent) { if (_PreviewSettingsDialog.isVisible()) { _hideSettingsDialog(); } else { _showSettingsDialog(); } clickEvent.preventDefault(); } /** * Displays the setting panel/dialog once the current user preferences have been loaded (if available). * The event handlers that allow to close the dialog are registered here. * @private * @author Loïs Bégué */ function _showSettingsDialog(){ // try to load the current values for the user settings if (_onLoadPreferencesValues && typeof(_onLoadPreferencesValues) === "function") { _onLoadPreferencesValues(_PreviewSettingsDialog); } // position and show the setting dialog _PreviewSettingsDialog.css({right: 22, top: _PreviewSettingsIcon.position().top + _PreviewSettingsIcon.outerHeight() + 12, padding: 0}); _PreviewSettingsDialog.show(); // Register the event handlers, that will allow to close the settings dialog when it's open _previewPanel.iFrame[0].contentDocument.body.addEventListener("click", _frameContentClickedEvent, true); $(window.document).on("click", _documentClickToCloseSettingsEvent); } /** * Hides the setting panel/dialog. This function is used by event handlers ('click', 'ESC' key...) that fire when the dialog should be closed. * @private * @author Loïs Bégué */ function _hideSettingsDialog(){ _PreviewSettingsDialog.hide(); // Unregister the event handlers that are in charge of closing the settings dialog _previewPanel.iFrame[0].contentDocument.body.removeEventListener("click", _frameContentClickedEvent, true); $(window.document).off("click", null, _documentClickToCloseSettingsEvent); } /** * Check if the user has clicked the setting dialog itself or the 'gear' icon above the dialog (not the preview document below them). * This function is called by click event handlers that should pass the issuing event as input parameter. * This fnction is used to prevent the dialog to be closed on an unintended click. * @private * @author Loïs Bégué * @param {object} clickEvent the event object provided by an event handler. * @returns {boolean} TRUE when a clicks occured directly within the settings panel/dialog. FALSE otherwise. */ function _isSettingsClicked(clickEvent){ return (_PreviewSettingsDialog.is(clickEvent.target) || _PreviewSettingsIcon.is(clickEvent.target) || _PreviewSettingsDialog.has(clickEvent.target).length > 0); } /******************************************** * PREVIEW PANEL creation ********************************************/ function createPreviewPanel(ID, onInitializeSettingsWithPreferences, onLoadPreferencesValuesIntoSettingsDialog) { // Kind of a "singleton" pattern if (!_previewPanel) { // ++++++++ SETUP PREVIEW PANEL +++++++++++++++++++++++++++++++++++++++++++ let panelHTML = require("text!../templates/panel.html"); let panelContent = $(panelHTML); _previewPanel = WorkspaceManager.createBottomPanel(ID, panelContent, 100); _previewPanel.ID = ID; _previewPanel.panel = panelContent; _previewPanel.iFrame = panelContent.find("#" + ID + "-frame"); _previewSpinner = panelContent.find("#" + ID + "-spinner"); _previewPanel.showSpinner = showSpinner; _previewPanel.hideSpinner = hideSpinner; _previewPanel.showPanel = showPreviewPanel; _previewPanel.hidePanel = hidePreviewPanel; _previewPanel.isVisible = isPanelVisible; _previewPanel.isContentVisible = isContentVisible; _previewPanel.hideContent = hidePreviewContent; _previewPanel.showContent = showPreviewContent; _previewPanel.setHTMLContent = setHTMLContent; _previewPanel.setHTMLSource = setHTMLSource; _previewPanel.scroll = scroll; _previewPanel.showDetachedPreview = showDetachedPreview; // ++++++++ SETUP SETTINGS DIALOG & ICON ++++++++++++++++++++++++++++++++++ let settingsHTML = require("text!../templates/settings.html"); _PreviewSettingsIcon = $("#" + ID + "-settings-icon").on("click", _settingsIconClickedEvent); _PreviewSettingsDialog = $(settingsHTML).appendTo(_previewPanel.panel).hide(); _onLoadPreferencesValues = onLoadPreferencesValuesIntoSettingsDialog; // Register callback for the initialization of the settings dialog if (onInitializeSettingsWithPreferences && typeof(onInitializeSettingsWithPreferences) === "function") { onInitializeSettingsWithPreferences(_PreviewSettingsDialog); } // ++++++++ SETUP PERMANENENT EVENT HANDLERS ++++++++++++++++++++++++++++++ _registerPermanentEventHandlers(); } return _previewPanel; } exports.createPreviewPanel = createPreviewPanel; });