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 5 import {View} from "./view.js" 6 import {anyToString, ViewElements, isIterable} from "./util.js" 7 import {MySelection} from "./selection.js" 8 9 export 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