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