1// Copyright (c) Jupyter Development Team. 2// Distributed under the terms of the Modified BSD License. 3'use strict'; 4 5import * as alertify from 'alertify.js'; 6 7import { 8 URLExt 9} from '@jupyterlab/coreutils/lib/url'; 10 11import { 12 Widget 13} from '@lumino/widgets'; 14 15import { 16 NotifyUserError 17} from 'nbdime/lib/common/exceptions'; 18 19import { 20 UNCHANGED_DIFF_CLASS, CHUNK_PANEL_CLASS 21} from 'nbdime/lib/diff/widget/common'; 22 23import { 24 UNCHANGED_MERGE_CLASS 25} from 'nbdime/lib/merge/widget/common'; 26 27import { 28 CELLDIFF_CLASS 29} from 'nbdime/lib/diff/widget'; 30 31import { 32 CELLMERGE_CLASS 33} from 'nbdime/lib/merge/widget'; 34 35 36/** 37 * DOM class for whether or not to hide unchanged cells 38 */ 39const HIDE_UNCHANGED_CLASS = 'jp-mod-hideUnchanged'; 40 41/** 42 * Global config data for the Nbdime application. 43 */ 44let configData: any = null; 45 46// Ensure error messages stay open until dismissed. 47alertify.delay(0).closeLogOnClick(true); 48 49/** 50 * Make an object fully immutable by freezing each object in it. 51 */ 52function deepFreeze(obj: any): any { 53 54 // Freeze properties before freezing self 55 Object.getOwnPropertyNames(obj).forEach(function(name) { 56 let prop = obj[name]; 57 58 // Freeze prop if it is an object 59 if (typeof prop === 'object' && prop !== null && !Object.isFrozen(prop)) { 60 deepFreeze(prop); 61 } 62 }); 63 64 // Freeze self 65 return Object.freeze(obj); 66} 67 68/** 69 * Retrive a config option 70 */ 71export 72function getConfigOption(name: string, defaultValue?: any): any { 73 if (configData) { 74 let ret = configData[name]; 75 if (ret === undefined) { 76 return defaultValue; 77 } 78 return ret; 79 } 80 if (typeof document !== 'undefined') { 81 let el = document.getElementById('nbdime-config-data'); 82 if (el && el.textContent) { 83 configData = JSON.parse(el.textContent); 84 } else { 85 configData = {}; 86 } 87 } 88 configData = deepFreeze(configData); 89 let ret = configData[name]; 90 if (ret === undefined) { 91 return defaultValue; 92 } 93 return ret; 94} 95 96/** 97 * Get the base url. 98 */ 99export 100function getBaseUrl(): string { 101 return URLExt.join(window.location.origin, getConfigOption('baseUrl')); 102} 103 104const spinner = document.createElement('div'); 105spinner.className = 'nbdime-spinner'; 106/** 107 * Turn spinner (loading indicator) on/off 108 */ 109export 110function toggleSpinner(state?: boolean) { 111 let header = document.getElementById('nbdime-header-buttonrow')!; 112 // Figure out current state 113 let current = header.contains(spinner); 114 if (state === undefined) { 115 state = !current; 116 } else if (state === current) { 117 return; // Nothing to do 118 } 119 if (state) { 120 header.appendChild(spinner); 121 } else { 122 header.removeChild(spinner); 123 } 124} 125 126 127/** 128 * Toggle whether to show or hide unchanged cells. 129 * 130 * This simply marks with a class, real work is done by CSS. 131 */ 132export 133function toggleShowUnchanged(show?: boolean, updateWidget?: Widget | null) { 134 let root = document.getElementById('nbdime-root')!; 135 let hiding = root.classList.contains(HIDE_UNCHANGED_CLASS); 136 if (show === undefined) { 137 show = hiding; 138 } else if (hiding !== show) { 139 // Nothing to do 140 return; 141 } 142 if (show) { 143 root.classList.remove(HIDE_UNCHANGED_CLASS); 144 if (updateWidget) { 145 updateWidget.update(); 146 } 147 } else { 148 markUnchangedRanges(); 149 root.classList.add(HIDE_UNCHANGED_CLASS); 150 } 151} 152 153 154/** 155 * Gets the chunk element of an added/removed cell, or the cell element for others 156 * @param cellElement 157 */ 158function getChunkElement(cellElement: Element): Element { 159 if (!cellElement.parentElement || !cellElement.parentElement.parentElement) { 160 return cellElement; 161 } 162 let chunkCandidate = cellElement.parentElement.parentElement; 163 if (chunkCandidate.classList.contains(CHUNK_PANEL_CLASS)) { 164 return chunkCandidate; 165 } 166 return cellElement; 167} 168 169 170/** 171 * Marks certain cells with 172 */ 173export 174function markUnchangedRanges() { 175 let root = document.getElementById('nbdime-root')!; 176 let children = root.querySelectorAll(`.${CELLDIFF_CLASS}, .${CELLMERGE_CLASS}`); 177 let rangeStart = -1; 178 for (let i=0; i < children.length; ++i) { 179 let child = children[i]; 180 if (!child.classList.contains(UNCHANGED_DIFF_CLASS) && 181 !child.classList.contains(UNCHANGED_MERGE_CLASS)) { 182 // Visible 183 if (rangeStart !== -1) { 184 // Previous was hidden 185 let N = i - rangeStart; 186 // Set attribute on element / chunk element as appropriate 187 getChunkElement(child).setAttribute('data-nbdime-NCellsHiddenBefore', N.toString()); 188 rangeStart = -1; 189 } 190 } else if (rangeStart === -1) { 191 rangeStart = i; 192 } 193 } 194 if (rangeStart !== -1) { 195 // Last element was part of a hidden range, need to mark 196 // the last cell that will be visible. 197 let N = children.length - rangeStart; 198 if (rangeStart === 0) { 199 // All elements were hidden, nothing to mark 200 // Add info on root instead 201 let tag = root.querySelector('.jp-Notebook-diff, .jp-Notebook-merge') || root; 202 tag.setAttribute('data-nbdime-AllCellsHidden', N.toString()); 203 return; 204 } 205 let lastVisible = children[rangeStart - 1]; 206 // Set attribute on element / chunk element as appropriate 207 getChunkElement(lastVisible).setAttribute('data-nbdime-NCellsHiddenAfter', N.toString()); 208 } 209} 210 211 212export let toolClosed = false; 213/** 214 * POSTs to the server that it should shut down if it was launched as a 215 * difftool/mergetool. 216 * 217 * Used to indicate that the tool has finished its operation, and that the tool 218 * should return to its caller. 219 */ 220export 221function closeTool(exitCode=0) { 222 if (!toolClosed) { 223 toolClosed = true; 224 let url = '/api/closetool'; 225 navigator.sendBeacon(url, JSON.stringify({exitCode})); 226 window.close(); 227 } 228} 229 230 231function showError(error: NotifyUserError, url: string, line: number, column: number) { 232 let message = error.message.replace('\n', '</br>'); 233 switch (error.severity) { 234 case 'warning': 235 alertify.log(message); 236 break; 237 case 'error': 238 alertify.error(message); 239 break; 240 default: 241 alertify.error(message); 242 } 243} 244 245export 246function handleError(msg: string, url: string, line: number, col?: number, error?: Error): boolean { 247 try { 248 if (error instanceof NotifyUserError) { 249 showError(error, url, line, col || 0); 250 return false; // Suppress error alert 251 } 252 } catch (e) { 253 // Not something that user should care about 254 console.log(e.stack); 255 } 256 return false; // Do not suppress default error alert 257} 258