1// Copyright 2015 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import {View} from "./view.js"
6import {anyToString, ViewElements, isIterable} from "./util.js"
7import {MySelection} from "./selection.js"
8
9export abstract class TextView extends View {
10  selectionHandler: NodeSelectionHandler;
11  blockSelectionHandler: BlockSelectionHandler;
12  nodeSelectionHandler: NodeSelectionHandler;
13  selection: MySelection;
14  blockSelection: MySelection;
15  textListNode: HTMLUListElement;
16  nodeIdToHtmlElementsMap: Map<string, Array<HTMLElement>>;
17  blockIdToHtmlElementsMap: Map<string, Array<HTMLElement>>;
18  blockIdtoNodeIds: Map<string, Array<string>>;
19  nodeIdToBlockId: Array<string>;
20  patterns: any;
21
22  constructor(id, broker, patterns) {
23    super(id);
24    let view = this;
25    view.textListNode = view.divNode.getElementsByTagName('ul')[0];
26    view.patterns = patterns;
27    view.nodeIdToHtmlElementsMap = new Map();
28    view.blockIdToHtmlElementsMap = new Map();
29    view.blockIdtoNodeIds = new Map();
30    view.nodeIdToBlockId = [];
31    view.selection = new MySelection(anyToString);
32    view.blockSelection = new MySelection(anyToString);
33    const selectionHandler = {
34      clear: function () {
35        view.selection.clear();
36        view.updateSelection();
37        broker.broadcastClear(selectionHandler);
38      },
39      select: function (nodeIds, selected) {
40        view.selection.select(nodeIds, selected);
41        view.updateSelection();
42        broker.broadcastNodeSelect(selectionHandler, view.selection.selectedKeys(), selected);
43      },
44      brokeredNodeSelect: function (nodeIds, selected) {
45        const firstSelect = view.blockSelection.isEmpty();
46        view.selection.select(nodeIds, selected);
47        view.updateSelection(firstSelect);
48      },
49      brokeredClear: function () {
50        view.selection.clear();
51        view.updateSelection();
52      }
53    };
54    this.selectionHandler = selectionHandler;
55    broker.addNodeHandler(selectionHandler);
56    view.divNode.onmouseup = function (e) {
57      if (!e.shiftKey) {
58        view.selectionHandler.clear();
59      }
60    }
61    const blockSelectionHandler = {
62      clear: function () {
63        view.blockSelection.clear();
64        view.updateSelection();
65        broker.broadcastClear(blockSelectionHandler);
66      },
67      select: function (blockIds, selected) {
68        view.blockSelection.select(blockIds, selected);
69        view.updateSelection();
70        broker.broadcastBlockSelect(blockSelectionHandler, blockIds, selected);
71      },
72      brokeredBlockSelect: function (blockIds, selected) {
73        const firstSelect = view.blockSelection.isEmpty();
74        view.blockSelection.select(blockIds, selected);
75        view.updateSelection(firstSelect);
76      },
77      brokeredClear: function () {
78        view.blockSelection.clear();
79        view.updateSelection();
80      }
81    };
82    this.blockSelectionHandler = blockSelectionHandler;
83    broker.addBlockHandler(blockSelectionHandler);
84  }
85
86  addHtmlElementForNodeId(anyNodeId: any, htmlElement: HTMLElement) {
87    const nodeId = anyToString(anyNodeId);
88    if (!this.nodeIdToHtmlElementsMap.has(nodeId)) {
89      this.nodeIdToHtmlElementsMap.set(nodeId, []);
90    }
91    this.nodeIdToHtmlElementsMap.get(nodeId).push(htmlElement);
92  }
93
94  addHtmlElementForBlockId(anyBlockId, htmlElement) {
95    const blockId = anyToString(anyBlockId);
96    if (!this.blockIdToHtmlElementsMap.has(blockId)) {
97      this.blockIdToHtmlElementsMap.set(blockId, []);
98    }
99    this.blockIdToHtmlElementsMap.get(blockId).push(htmlElement);
100  }
101
102  addNodeIdToBlockId(anyNodeId, anyBlockId) {
103    const blockId = anyToString(anyBlockId);
104    if (!this.blockIdtoNodeIds.has(blockId)) {
105      this.blockIdtoNodeIds.set(blockId, []);
106    }
107    this.blockIdtoNodeIds.get(blockId).push(anyToString(anyNodeId));
108    this.nodeIdToBlockId[anyNodeId] = blockId;
109  }
110
111  blockIdsForNodeIds(nodeIds) {
112    const blockIds = [];
113    for (const nodeId of nodeIds) {
114      const blockId = this.nodeIdToBlockId[nodeId];
115      if (blockId == undefined) continue;
116      blockIds.push(blockId);
117    }
118    return blockIds;
119  }
120
121  updateSelection(scrollIntoView: boolean = false) {
122    if (this.divNode.parentNode == null) return;
123    const mkVisible = new ViewElements(this.divNode.parentNode as HTMLElement);
124    const view = this;
125    for (const [blockId, elements] of this.blockIdToHtmlElementsMap.entries()) {
126      const isSelected = view.blockSelection.isSelected(blockId);
127      for (const element of elements) {
128        mkVisible.consider(element, isSelected);
129        element.classList.toggle("selected", isSelected);
130      }
131    }
132    for (const key of this.nodeIdToHtmlElementsMap.keys()) {
133      for (const element of this.nodeIdToHtmlElementsMap.get(key)) {
134        element.classList.toggle("selected", false);
135      }
136    }
137    for (const nodeId of view.selection.selectedKeys()) {
138      const elements = this.nodeIdToHtmlElementsMap.get(nodeId);
139      if (!elements) continue;
140      for (const element of elements) {
141        mkVisible.consider(element, true);
142        element.classList.toggle("selected", true);
143      }
144    }
145    mkVisible.apply(scrollIntoView);
146  }
147
148  setPatterns(patterns) {
149    let view = this;
150    view.patterns = patterns;
151  }
152
153  clearText() {
154    let view = this;
155    while (view.textListNode.firstChild) {
156      view.textListNode.removeChild(view.textListNode.firstChild);
157    }
158  }
159
160  createFragment(text, style) {
161    let view = this;
162    let fragment = document.createElement("SPAN");
163
164    if (style.blockId != undefined) {
165      const blockId = style.blockId(text);
166      if (blockId != undefined) {
167        fragment.blockId = blockId;
168        this.addHtmlElementForBlockId(blockId, fragment);
169      }
170    }
171
172    if (typeof style.link == 'function') {
173      fragment.classList.add('linkable-text');
174      fragment.onmouseup = function (e) {
175        e.stopPropagation();
176        style.link(text)
177      };
178    }
179
180    if (typeof style.nodeId == 'function') {
181      const nodeId = style.nodeId(text);
182      if (nodeId != undefined) {
183        fragment.nodeId = nodeId;
184        this.addHtmlElementForNodeId(nodeId, fragment);
185      }
186    }
187
188    if (typeof style.assignBlockId === 'function') {
189      fragment.blockId = style.assignBlockId();
190      this.addNodeIdToBlockId(fragment.nodeId, fragment.blockId);
191    }
192
193    if (typeof style.linkHandler == 'function') {
194      const handler = style.linkHandler(text, fragment)
195      if (handler !== undefined) {
196        fragment.classList.add('linkable-text');
197        fragment.onmouseup = handler;
198      }
199    }
200
201    if (style.css != undefined) {
202      const css = isIterable(style.css) ? style.css : [style.css];
203      for (const cls of css) {
204        fragment.classList.add(cls);
205      }
206    }
207    fragment.innerHTML = text;
208    return fragment;
209  }
210
211  processLine(line) {
212    let view = this;
213    let result = [];
214    let patternSet = 0;
215    while (true) {
216      let beforeLine = line;
217      for (let pattern of view.patterns[patternSet]) {
218        let matches = line.match(pattern[0]);
219        if (matches != null) {
220          if (matches[0] != '') {
221            let style = pattern[1] != null ? pattern[1] : {};
222            let text = matches[0];
223            if (text != '') {
224              let fragment = view.createFragment(matches[0], style);
225              result.push(fragment);
226            }
227            line = line.substr(matches[0].length);
228          }
229          let nextPatternSet = patternSet;
230          if (pattern.length > 2) {
231            nextPatternSet = pattern[2];
232          }
233          if (line == "") {
234            if (nextPatternSet != -1) {
235              throw ("illegal parsing state in text-view in patternSet" + patternSet);
236            }
237            return result;
238          }
239          patternSet = nextPatternSet;
240          break;
241        }
242      }
243      if (beforeLine == line) {
244        throw ("input not consumed in text-view in patternSet" + patternSet);
245      }
246    }
247  }
248
249  processText(text) {
250    let view = this;
251    let textLines = text.split(/[\n]/);
252    let lineNo = 0;
253    for (let line of textLines) {
254      let li = document.createElement("LI");
255      li.className = "nolinenums";
256      li.dataset.lineNo = "" + lineNo++;
257      let fragments = view.processLine(line);
258      for (let fragment of fragments) {
259        li.appendChild(fragment);
260      }
261      view.textListNode.appendChild(li);
262    }
263  }
264
265  initializeContent(data, rememberedSelection) {
266    let view = this;
267    view.clearText();
268    view.processText(data);
269  }
270
271  deleteContent() {
272  }
273
274  isScrollable() {
275    return true;
276  }
277}
278