@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
JavaScript
/**
* @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;
});