[go: nahoru, domu]

blob: 64d9ebb073dd197289d8242e570481b404508aad [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no_underscored_properties */
import * as i18n from '../../../../core/i18n/i18n.js';
import * as Platform from '../../../../core/platform/platform.js';
import * as TextUtils from '../../../../models/text_utils/text_utils.js';
import * as UI from '../../legacy.js';
const UIStrings = {
/**
*@description Text to find an item
*/
find: 'Find',
};
const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/source_frame/XMLView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class XMLView extends UI.Widget.Widget implements UI.SearchableView.Searchable {
_treeOutline: UI.TreeOutline.TreeOutlineInShadow;
_searchableView!: UI.SearchableView.SearchableView|null;
_currentSearchFocusIndex: number;
_currentSearchTreeElements: XMLViewNode[];
_searchConfig!: UI.SearchableView.SearchConfig|null;
constructor(parsedXML: Document) {
super(true);
this.registerRequiredCSS('ui/legacy/components/source_frame/xmlView.css', {enableLegacyPatching: false});
this.contentElement.classList.add('shadow-xml-view', 'source-code');
this._treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
this._treeOutline.registerRequiredCSS(
'ui/legacy/components/source_frame/xmlTree.css', {enableLegacyPatching: true});
this.contentElement.appendChild(this._treeOutline.element);
this._currentSearchFocusIndex = 0;
this._currentSearchTreeElements = [];
XMLViewNode.populate(this._treeOutline, parsedXML, this);
const firstChild = this._treeOutline.firstChild();
if (firstChild) {
firstChild.select(true /* omitFocus */, false /* selectedByUser */);
}
}
static createSearchableView(parsedXML: Document): UI.SearchableView.SearchableView {
const xmlView = new XMLView(parsedXML);
const searchableView = new UI.SearchableView.SearchableView(xmlView, null);
searchableView.setPlaceholder(i18nString(UIStrings.find));
xmlView._searchableView = searchableView;
xmlView.show(searchableView.element);
return searchableView;
}
static parseXML(text: string, mimeType: string): Document|null {
let parsedXML;
try {
switch (mimeType) {
case 'application/xhtml+xml':
case 'application/xml':
case 'image/svg+xml':
case 'text/html':
case 'text/xml':
parsedXML = (new DOMParser()).parseFromString(text, mimeType);
}
} catch (e) {
return null;
}
if (!parsedXML || parsedXML.body) {
return null;
}
return parsedXML;
}
_jumpToMatch(index: number, shouldJump: boolean): void {
if (!this._searchConfig) {
return;
}
const regex = this._searchConfig.toSearchRegex(true);
const previousFocusElement = this._currentSearchTreeElements[this._currentSearchFocusIndex];
if (previousFocusElement) {
previousFocusElement.setSearchRegex(regex);
}
const newFocusElement = this._currentSearchTreeElements[index];
if (newFocusElement) {
this._updateSearchIndex(index);
if (shouldJump) {
newFocusElement.reveal(true);
}
newFocusElement.setSearchRegex(regex, UI.UIUtils.highlightedCurrentSearchResultClassName);
} else {
this._updateSearchIndex(0);
}
}
_updateSearchCount(count: number): void {
if (!this._searchableView) {
return;
}
this._searchableView.updateSearchMatchesCount(count);
}
_updateSearchIndex(index: number): void {
this._currentSearchFocusIndex = index;
if (!this._searchableView) {
return;
}
this._searchableView.updateCurrentMatchIndex(index);
}
_innerPerformSearch(shouldJump: boolean, jumpBackwards?: boolean): void {
if (!this._searchConfig) {
return;
}
let newIndex: number = this._currentSearchFocusIndex;
const previousSearchFocusElement = this._currentSearchTreeElements[newIndex];
this._innerSearchCanceled();
this._currentSearchTreeElements = [];
const regex = this._searchConfig.toSearchRegex(true);
for (let element: (UI.TreeOutline.TreeElement|null) =
(this._treeOutline.rootElement() as UI.TreeOutline.TreeElement | null);
element; element = element.traverseNextTreeElement(false)) {
if (!(element instanceof XMLViewNode)) {
continue;
}
const hasMatch = element.setSearchRegex(regex);
if (hasMatch) {
this._currentSearchTreeElements.push(element);
}
if (previousSearchFocusElement === element) {
const currentIndex = this._currentSearchTreeElements.length - 1;
if (hasMatch || jumpBackwards) {
newIndex = currentIndex;
} else {
newIndex = currentIndex + 1;
}
}
}
this._updateSearchCount(this._currentSearchTreeElements.length);
if (!this._currentSearchTreeElements.length) {
this._updateSearchIndex(0);
return;
}
newIndex = Platform.NumberUtilities.mod(newIndex, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, shouldJump);
}
_innerSearchCanceled(): void {
for (let element: (UI.TreeOutline.TreeElement|null) =
(this._treeOutline.rootElement() as UI.TreeOutline.TreeElement | null);
element; element = element.traverseNextTreeElement(false)) {
if (!(element instanceof XMLViewNode)) {
continue;
}
element.revertHighlightChanges();
}
this._updateSearchCount(0);
this._updateSearchIndex(0);
}
searchCanceled(): void {
this._searchConfig = null;
this._currentSearchTreeElements = [];
this._innerSearchCanceled();
}
performSearch(searchConfig: UI.SearchableView.SearchConfig, shouldJump: boolean, jumpBackwards?: boolean): void {
this._searchConfig = searchConfig;
this._innerPerformSearch(shouldJump, jumpBackwards);
}
jumpToNextSearchResult(): void {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex =
Platform.NumberUtilities.mod(this._currentSearchFocusIndex + 1, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, true);
}
jumpToPreviousSearchResult(): void {
if (!this._currentSearchTreeElements.length) {
return;
}
const newIndex =
Platform.NumberUtilities.mod(this._currentSearchFocusIndex - 1, this._currentSearchTreeElements.length);
this._jumpToMatch(newIndex, true);
}
supportsCaseSensitiveSearch(): boolean {
return true;
}
supportsRegexSearch(): boolean {
return true;
}
}
export class XMLViewNode extends UI.TreeOutline.TreeElement {
_node: Node|ParentNode;
_closeTag: boolean;
_highlightChanges: UI.UIUtils.HighlightChange[];
_xmlView: XMLView;
constructor(node: Node|ParentNode, closeTag: boolean, xmlView: XMLView) {
super('', !closeTag && 'childElementCount' in node && Boolean(node.childElementCount));
this._node = node;
this._closeTag = closeTag;
this.selectable = true;
this._highlightChanges = [];
this._xmlView = xmlView;
this._updateTitle();
}
static populate(
root: UI.TreeOutline.TreeOutline|UI.TreeOutline.TreeElement, xmlNode: Node|ParentNode, xmlView: XMLView): void {
if (!(xmlNode instanceof Node)) {
return;
}
let node: (ChildNode|null) = xmlNode.firstChild;
while (node) {
const currentNode = node;
node = node.nextSibling;
const nodeType = currentNode.nodeType;
// ignore empty TEXT
if (nodeType === 3 && currentNode.nodeValue && currentNode.nodeValue.match(/\s+/)) {
continue;
}
// ignore ATTRIBUTE, ENTITY_REFERENCE, ENTITY, DOCUMENT, DOCUMENT_TYPE, DOCUMENT_FRAGMENT, NOTATION
if ((nodeType !== 1) && (nodeType !== 3) && (nodeType !== 4) && (nodeType !== 7) && (nodeType !== 8)) {
continue;
}
root.appendChild(new XMLViewNode(currentNode, false, xmlView));
}
}
setSearchRegex(regex: RegExp|null, additionalCssClassName?: string): boolean {
this.revertHighlightChanges();
if (!regex) {
return false;
}
if (this._closeTag && this.parent && !this.parent.expanded) {
return false;
}
regex.lastIndex = 0;
let cssClasses = UI.UIUtils.highlightedSearchResultClassName;
if (additionalCssClassName) {
cssClasses += ' ' + additionalCssClassName;
}
if (!this.listItemElement.textContent) {
return false;
}
const content = this.listItemElement.textContent.replace(/\xA0/g, ' ');
let match = regex.exec(content);
const ranges = [];
while (match) {
ranges.push(new TextUtils.TextRange.SourceRange(match.index, match[0].length));
match = regex.exec(content);
}
if (ranges.length) {
UI.UIUtils.highlightRangesWithStyleClass(this.listItemElement, ranges, cssClasses, this._highlightChanges);
}
return Boolean(this._highlightChanges.length);
}
revertHighlightChanges(): void {
UI.UIUtils.revertDomChanges(this._highlightChanges);
this._highlightChanges = [];
}
_updateTitle(): void {
const node = this._node;
if (!('nodeType' in node)) {
return;
}
switch (node.nodeType) {
case 1: { // ELEMENT
if (node instanceof Element) {
const tag = node.tagName;
if (this._closeTag) {
this._setTitle(['</' + tag + '>', 'shadow-xml-view-tag']);
return;
}
const titleItems = ['<' + tag, 'shadow-xml-view-tag'];
const attributes = node.attributes;
for (let i = 0; i < attributes.length; ++i) {
const attributeNode = attributes.item(i);
if (!attributeNode) {
return;
}
titleItems.push(
'\xA0', 'shadow-xml-view-tag', attributeNode.name, 'shadow-xml-view-attribute-name', '="',
'shadow-xml-view-tag', attributeNode.value, 'shadow-xml-view-attribute-value', '"',
'shadow-xml-view-tag');
}
if (!this.expanded) {
if (node.childElementCount) {
titleItems.push(
'>', 'shadow-xml-view-tag', '…', 'shadow-xml-view-comment', '</' + tag, 'shadow-xml-view-tag');
} else if (node.textContent) {
titleItems.push(
'>', 'shadow-xml-view-tag', node.textContent, 'shadow-xml-view-text', '</' + tag,
'shadow-xml-view-tag');
} else {
titleItems.push(' /', 'shadow-xml-view-tag');
}
}
titleItems.push('>', 'shadow-xml-view-tag');
this._setTitle(titleItems);
return;
}
return;
}
case 3: { // TEXT
if (node.nodeValue) {
this._setTitle([node.nodeValue, 'shadow-xml-view-text']);
}
return;
}
case 4: { // CDATA
if (node.nodeValue) {
this._setTitle([
'<![CDATA[',
'shadow-xml-view-cdata',
node.nodeValue,
'shadow-xml-view-text',
']]>',
'shadow-xml-view-cdata',
]);
}
return;
}
case 7: { // PROCESSING_INSTRUCTION
if (node.nodeValue) {
this._setTitle(
['<?' + node.nodeName + ' ' + node.nodeValue + '?>', 'shadow-xml-view-processing-instruction']);
}
return;
}
case 8: { // COMMENT
this._setTitle(['<!--' + node.nodeValue + '-->', 'shadow-xml-view-comment']);
return;
}
}
}
_setTitle(items: string[]): void {
const titleFragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i += 2) {
titleFragment.createChild('span', items[i + 1]).textContent = items[i];
}
this.title = titleFragment;
this._xmlView._innerPerformSearch(false, false);
}
onattach(): void {
this.listItemElement.classList.toggle('shadow-xml-view-close-tag', this._closeTag);
}
onexpand(): void {
this._updateTitle();
}
oncollapse(): void {
this._updateTitle();
}
async onpopulate(): Promise<void> {
XMLViewNode.populate(this, this._node, this._xmlView);
this.appendChild(new XMLViewNode(this._node, true, this._xmlView));
}
}