Markdown Overhaul (#2624)
Rebuilt Markdown editor/views into new components
This commit is contained in:
parent
945510dcf0
commit
6b54279bb7
50 changed files with 819 additions and 3896 deletions
|
@ -35,6 +35,8 @@
|
|||
"react-dom": "^15.3.2",
|
||||
"restangular": "^1.2.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"showdown": "^1.6.4",
|
||||
"showdown-highlightjs-extension": "^0.1.2",
|
||||
"underscore": "^1.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -47,6 +49,7 @@
|
|||
"@types/jquery": "^2.0.40",
|
||||
"@types/react": "0.14.39",
|
||||
"@types/react-dom": "0.14.17",
|
||||
"@types/showdown": "^1.4.32",
|
||||
"angular-mocks": "1.6.2",
|
||||
"angular-ts-decorators": "0.0.19",
|
||||
"css-loader": "0.25.0",
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
.markdown-editor-element .wmd-panel .btn {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.markdown-editor-element .wmd-panel .btn:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.markdown-editor-element .wmd-panel .btn:active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-btn {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-btn.active {
|
||||
box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-panel .markdown-view {
|
||||
border: 1px solid #eee;
|
||||
padding: 4px;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.markdown-editor-element .preview-top-bar {
|
||||
height: 43px;
|
||||
line-height: 43px;
|
||||
color: #ddd;
|
||||
}
|
|
@ -9,7 +9,9 @@
|
|||
}
|
||||
|
||||
.quay-service-status-description {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.quay-service-status-indicator.none {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<!-- Comment -->
|
||||
<div class="image-comment" ng-if="imageData.comment">
|
||||
<blockquote style="margin-top: 10px;">
|
||||
<span class="markdown-view" content="imageData.comment"></span>
|
||||
<markdown-view content="imageData.comment"></markdown-view>
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<div class="markdown-editor-element">
|
||||
<a class="btn btn-default preview-btn" ng-click="togglePreview()" ng-class="{'active': previewing}">Preview</a>
|
||||
<div class="wmd-panel" ng-show="!previewing">
|
||||
<div id="wmd-button-bar-{{id}}"></div>
|
||||
<textarea class="wmd-input form-control" id="wmd-input-{{id}}" ng-model="content"></textarea>
|
||||
</div>
|
||||
<div class="preview-panel" ng-show="previewing">
|
||||
<div class="preview-top-bar">Viewing preview</div>
|
||||
<div class="markdown-view" content="content || '(Nothing entered)'"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,31 +0,0 @@
|
|||
<div class="markdown-input-container">
|
||||
<p ng-class="'lead ' + (canWrite ? 'editable' : 'noteditable')" ng-click="editContent()">
|
||||
<span class="markdown-view" content="content"></span>
|
||||
<span class="empty" ng-show="!content && canWrite">Click to set {{ fieldTitle }}</span>
|
||||
<i class="fa fa-edit"></i>
|
||||
</p>
|
||||
|
||||
<!-- Modal editor -->
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Edit {{ fieldTitle }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="wmd-panel">
|
||||
<div id="wmd-button-bar-description-{{id}}"></div>
|
||||
<textarea class="wmd-input" id="wmd-input-description-{{id}}" placeholder="Enter {{ fieldTitle }}">{{ content }}</textarea>
|
||||
</div>
|
||||
|
||||
<div id="wmd-preview-description-{{id}}" class="wmd-panel wmd-preview"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-click="saveContent()">Save changes</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
|
@ -1 +0,0 @@
|
|||
<span class="markdown-view-content" ng-bind-html="getMarkedDown(content, firstLineOnly)"></span>
|
|
@ -3,7 +3,7 @@
|
|||
<div class="quay-service-status-description" ng-class="message.severity">
|
||||
<span ng-switch on="message.media_type">
|
||||
<span ng-switch-when="text/markdown">
|
||||
<span class="markdown-view" content="message.content"></span>
|
||||
<markdown-view content="message.content"></markdown-view>
|
||||
</span>
|
||||
<span ng-switch-default>{{ message.content }}</span>
|
||||
</span>
|
||||
|
|
|
@ -41,7 +41,10 @@
|
|||
star-toggled="starToggled({'repository': repository})"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description markdown-view" content="repository.description" first-line-only="true" placeholder-needed="true"></div>
|
||||
<markdown-view class="description"
|
||||
content="repository.description"
|
||||
first-line-only="true"
|
||||
placeholder-needed="true"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -57,11 +57,11 @@
|
|||
<tr>
|
||||
<td>
|
||||
<h4 style="font-size:20px;">Description</h4>
|
||||
<div class="description markdown-input"
|
||||
content="repository.description"
|
||||
can-write="repository.can_write"
|
||||
content-changed="updateDescription"
|
||||
field-title="'repository description'">
|
||||
<div class="description">
|
||||
<markdown-input content="repository.description"
|
||||
can-write="repository.can_write"
|
||||
(content-changed)="updateDescription($event.content)"
|
||||
field-title="repository description"></markdown-input>
|
||||
</div>
|
||||
</td>
|
||||
<td style="width: 400px;" class="hidden-xs hidden-sm">
|
||||
|
|
|
@ -167,7 +167,7 @@
|
|||
|
||||
<div bo-if="key.approval.notes">
|
||||
<div class="subtitle">Approval notes</div>
|
||||
<div class="markdown-view" content="key.approval.notes"></div>
|
||||
<markdown-view content="key.approval.notes"></markdown-view>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
19
static/js/constants/platform.constant.ts
Normal file
19
static/js/constants/platform.constant.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Type representing current browser platform.
|
||||
* TODO: Add more browser platforms.
|
||||
*/
|
||||
export type BrowserPlatform = "firefox"
|
||||
| "chrome";
|
||||
|
||||
/**
|
||||
* Constant representing current browser platform. Used for determining available features.
|
||||
* TODO Only rudimentary implementation, should prefer specific feature detection strategies instead.
|
||||
*/
|
||||
export const browserPlatform: BrowserPlatform = (() => {
|
||||
if (navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
|
||||
return 'firefox';
|
||||
}
|
||||
else {
|
||||
return 'chrome';
|
||||
}
|
||||
})();
|
|
@ -50,4 +50,3 @@ angular.module('quay').directive('repoPanelInfo', function () {
|
|||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
|
|
@ -30,11 +30,11 @@
|
|||
<cor-tab-content>
|
||||
<!-- Description -->
|
||||
<cor-tab-pane id="description">
|
||||
<div class="description markdown-input"
|
||||
content="$ctrl.repository.description"
|
||||
can-write="$ctrl.repository.can_write"
|
||||
content-changed="$ctrl.updateDescription"
|
||||
field-title="'application description'">
|
||||
<div class="description">
|
||||
<markdown-input content="$ctrl.repository.description"
|
||||
can-write="$ctrl.repository.can_write"
|
||||
(content-changed)="$ctrl.updateDescription($event.content)"
|
||||
field-title="repository description"></markdown-input>
|
||||
</div>
|
||||
</cor-tab-pane>
|
||||
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* An element which display an inline editor for writing and previewing markdown text.
|
||||
*/
|
||||
angular.module('quay').directive('markdownEditor', function () {
|
||||
var counter = 0;
|
||||
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/markdown-editor.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'content': '=content',
|
||||
},
|
||||
controller: function($scope, $element, $timeout) {
|
||||
$scope.id = (counter++);
|
||||
$scope.previewing = false;
|
||||
|
||||
$timeout(function() {
|
||||
var converter = Markdown.getSanitizingConverter();
|
||||
var editor = new Markdown.Editor(converter, '-' + $scope.id);
|
||||
editor.run();
|
||||
});
|
||||
|
||||
$scope.togglePreview = function() {
|
||||
$scope.previewing = !$scope.previewing;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,49 +0,0 @@
|
|||
/**
|
||||
* An element which allows for entry of markdown content and previewing its rendering.
|
||||
*/
|
||||
angular.module('quay').directive('markdownInput', function () {
|
||||
var counter = 0;
|
||||
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/markdown-input.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'content': '=content',
|
||||
'canWrite': '=canWrite',
|
||||
'contentChanged': '=contentChanged',
|
||||
'fieldTitle': '=fieldTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var elm = $element[0];
|
||||
|
||||
$scope.id = (counter++);
|
||||
|
||||
$scope.editContent = function() {
|
||||
if (!$scope.canWrite) { return; }
|
||||
|
||||
if (!$scope.markdownDescriptionEditor) {
|
||||
var converter = Markdown.getSanitizingConverter();
|
||||
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
|
||||
editor.run();
|
||||
$scope.markdownDescriptionEditor = editor;
|
||||
}
|
||||
|
||||
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
|
||||
$(elm).find('.modal').modal({});
|
||||
};
|
||||
|
||||
$scope.saveContent = function() {
|
||||
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
|
||||
$(elm).find('.modal').modal('hide');
|
||||
|
||||
if ($scope.contentChanged) {
|
||||
$scope.contentChanged($scope.content);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* An element which displays its content processed as markdown.
|
||||
*/
|
||||
angular.module('quay').directive('markdownView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/markdown-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'content': '=content',
|
||||
'firstLineOnly': '=firstLineOnly',
|
||||
'placeholderNeeded': '=placeholderNeeded'
|
||||
},
|
||||
controller: function($scope, $element, $sce, UtilService) {
|
||||
$scope.getMarkedDown = function(content, firstLineOnly) {
|
||||
if (firstLineOnly) {
|
||||
return $sce.trustAsHtml(UtilService.getFirstMarkdownLineAsText(content, $scope.placeholderNeeded));
|
||||
}
|
||||
return $sce.trustAsHtml(UtilService.getMarkedDown(content));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
.markdown-editor-element textarea {
|
||||
height: 300px;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons button:last-child {
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<div class="markdown-editor-element">
|
||||
<!-- Write/preview tabs -->
|
||||
<ul class="nav nav-tabs" style="width: 100%;">
|
||||
<li role="presentation" ng-class="$ctrl.editMode == 'write' ? 'active': ''"
|
||||
ng-click="$ctrl.changeEditMode('write')">
|
||||
<a href="#">Write</a>
|
||||
</li>
|
||||
<li role="presentation" ng-class="$ctrl.editMode == 'preview' ? 'active': ''"
|
||||
ng-click="$ctrl.changeEditMode('preview')">
|
||||
<a href="#">Preview</a>
|
||||
</li>
|
||||
<!-- Editing toolbar -->
|
||||
<li style="float: right;">
|
||||
<markdown-toolbar ng-if="$ctrl.editMode == 'write'"
|
||||
(insert-symbol)="$ctrl.insertSymbol($event)"></markdown-toolbar>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" style="padding: 10px 0 0 0;">
|
||||
<div ng-show="$ctrl.editMode == 'write'">
|
||||
<textarea id="markdown-textarea"
|
||||
placeholder="Enter {{ ::$ctrl.fieldTitle }}"
|
||||
ng-model="$ctrl.content"></textarea>
|
||||
</div>
|
||||
<div class="markdown-editor-preview"
|
||||
ng-if="$ctrl.editMode == 'preview'">
|
||||
<markdown-view content="$ctrl.content"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="markdown-editor-actions">
|
||||
<div class="markdown-editor-buttons">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="$ctrl.discardChanges()">
|
||||
Close
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-click="$ctrl.saveChanges()">
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,147 @@
|
|||
import { MarkdownEditorComponent, EditMode } from './markdown-editor.component';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownEditorComponent", () => {
|
||||
var component: MarkdownEditorComponent;
|
||||
var textarea: Mock<ng.IAugmentedJQuery | any>;
|
||||
var documentMock: Mock<HTMLElement & Document>;
|
||||
|
||||
beforeEach(() => {
|
||||
textarea = new Mock<ng.IAugmentedJQuery | any>();
|
||||
documentMock = new Mock<HTMLElement & Document>();
|
||||
const $documentMock: any = [documentMock.Object];
|
||||
component = new MarkdownEditorComponent($documentMock, 'chrome');
|
||||
component.textarea = textarea.Object;
|
||||
});
|
||||
|
||||
describe("changeEditMode", () => {
|
||||
|
||||
it("sets component's edit mode to given mode", () => {
|
||||
const editMode: EditMode = "preview";
|
||||
component.changeEditMode(editMode);
|
||||
|
||||
expect(component.currentEditMode).toEqual(editMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertSymbol", () => {
|
||||
var event: {symbol: MarkdownSymbol};
|
||||
var markdownSymbols: {type: MarkdownSymbol, characters: string, shiftBy: number}[];
|
||||
var innerText: string;
|
||||
|
||||
beforeEach(() => {
|
||||
event = {symbol: 'heading1'};
|
||||
innerText = "Here is some text";
|
||||
markdownSymbols = [
|
||||
{type: 'heading1', characters: '# ', shiftBy: 2},
|
||||
{type: 'heading2', characters: '## ', shiftBy: 3},
|
||||
{type: 'heading3', characters: '### ', shiftBy: 4},
|
||||
{type: 'bold', characters: '****', shiftBy: 2},
|
||||
{type: 'italics', characters: '__', shiftBy: 1},
|
||||
{type: 'bulleted-list', characters: '- ', shiftBy: 2},
|
||||
{type: 'numbered-list', characters: '1. ', shiftBy: 3},
|
||||
{type: 'quote', characters: '> ', shiftBy: 2},
|
||||
{type: 'link', characters: '[](url)', shiftBy: 1},
|
||||
{type: 'code', characters: '``', shiftBy: 1},
|
||||
];
|
||||
|
||||
textarea.setup(mock => mock.focus);
|
||||
textarea.setup(mock => mock.substr).is((start, end) => '');
|
||||
textarea.setup(mock => mock.val).is((value?) => innerText);
|
||||
textarea.setup(mock => mock.prop).is((prop) => {
|
||||
switch (prop) {
|
||||
case "selectionStart":
|
||||
return 0;
|
||||
case "selectionEnd":
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
documentMock.setup(mock => mock.execCommand).is((commandID, showUI, value) => false);
|
||||
});
|
||||
|
||||
it("focuses on markdown textarea", () => {
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect(<Spy>textarea.Object.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inserts correct characters for given symbol at cursor position", () => {
|
||||
markdownSymbols.forEach((symbol) => {
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText');
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false);
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(symbol.characters);
|
||||
|
||||
(<Spy>documentMock.Object.execCommand).calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
it("splices highlighted selection between inserted characters instead of deleting them", () => {
|
||||
markdownSymbols.slice(0, 1).forEach((symbol) => {
|
||||
textarea.setup(mock => mock.prop).is((prop) => {
|
||||
switch (prop) {
|
||||
case "selectionStart":
|
||||
return 0;
|
||||
case "selectionEnd":
|
||||
return innerText.length;
|
||||
}
|
||||
});
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText');
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false);
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(`${symbol.characters.slice(0, symbol.shiftBy)}${innerText}${symbol.characters.slice(symbol.shiftBy, symbol.characters.length)}`);
|
||||
|
||||
(<Spy>documentMock.Object.execCommand).calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
it("moves cursor to correct position for given symbol", () => {
|
||||
markdownSymbols.forEach((symbol) => {
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(2)[0]).toEqual('selectionStart');
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(2)[1]).toEqual(symbol.shiftBy);
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(3)[0]).toEqual('selectionEnd');
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(3)[1]).toEqual(symbol.shiftBy);
|
||||
|
||||
(<Spy>textarea.Object.prop).calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveChanges", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.content = "# Some markdown content";
|
||||
});
|
||||
|
||||
it("emits output event with changed content", (done) => {
|
||||
component.save.subscribe((event: {editedContent: string}) => {
|
||||
expect(event.editedContent).toEqual(component.content);
|
||||
done();
|
||||
});
|
||||
|
||||
component.saveChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe("discardChanges", () => {
|
||||
|
||||
it("emits output event with no content", (done) => {
|
||||
component.discard.subscribe((event: {}) => {
|
||||
expect(event).toEqual({});
|
||||
done();
|
||||
});
|
||||
|
||||
component.discardChanges();
|
||||
});
|
||||
});
|
||||
});
|
133
static/js/directives/ui/markdown/markdown-editor.component.ts
Normal file
133
static/js/directives/ui/markdown/markdown-editor.component.ts
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { Component, Inject, Input, Output, EventEmitter, ViewChild } from 'ng-metadata/core';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
import { BrowserPlatform } from '../../../constants/platform.constant';
|
||||
import './markdown-editor.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* An editing interface for Markdown content.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-editor',
|
||||
templateUrl: '/static/js/directives/ui/markdown/markdown-editor.component.html'
|
||||
})
|
||||
export class MarkdownEditorComponent {
|
||||
|
||||
@Input('<') public content: string;
|
||||
@Output() public save: EventEmitter<{editedContent: string}> = new EventEmitter();
|
||||
@Output() public discard: EventEmitter<any> = new EventEmitter();
|
||||
// Textarea is public for testability, should not be directly accessed
|
||||
@ViewChild('#markdown-textarea') public textarea: ng.IAugmentedJQuery;
|
||||
private editMode: EditMode = "write";
|
||||
|
||||
constructor(@Inject('$document') private $document: ng.IDocumentService,
|
||||
@Inject('BrowserPlatform') private browserPlatform: BrowserPlatform) {
|
||||
|
||||
}
|
||||
|
||||
public changeEditMode(newMode: EditMode): void {
|
||||
this.editMode = newMode;
|
||||
}
|
||||
|
||||
public insertSymbol(event: {symbol: MarkdownSymbol}): void {
|
||||
this.textarea.focus();
|
||||
|
||||
const startPos: number = this.textarea.prop('selectionStart');
|
||||
const endPos: number = this.textarea.prop('selectionEnd');
|
||||
const innerText: string = this.textarea.val().slice(startPos, endPos);
|
||||
var shiftBy: number = 0;
|
||||
var characters: string = '';
|
||||
|
||||
switch (event.symbol) {
|
||||
case 'heading1':
|
||||
characters = '# ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'heading2':
|
||||
characters = '## ';
|
||||
shiftBy = 3;
|
||||
break;
|
||||
case 'heading3':
|
||||
characters = '### ';
|
||||
shiftBy = 4;
|
||||
break;
|
||||
case 'bold':
|
||||
characters = '****';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'italics':
|
||||
characters = '__';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
case 'bulleted-list':
|
||||
characters = '- ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'numbered-list':
|
||||
characters = '1. ';
|
||||
shiftBy = 3;
|
||||
break;
|
||||
case 'quote':
|
||||
characters = '> ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'link':
|
||||
characters = '[](url)';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
case 'code':
|
||||
characters = '``';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
const cursorPos: number = startPos + shiftBy;
|
||||
|
||||
if (startPos != endPos) {
|
||||
this.insertText(`${characters.slice(0, shiftBy)}${innerText}${characters.slice(shiftBy, characters.length)}`,
|
||||
startPos,
|
||||
endPos);
|
||||
}
|
||||
else {
|
||||
this.insertText(characters, startPos, endPos);
|
||||
}
|
||||
|
||||
this.textarea.prop('selectionStart', cursorPos);
|
||||
this.textarea.prop('selectionEnd', cursorPos);
|
||||
}
|
||||
|
||||
public saveChanges(): void {
|
||||
this.save.emit({editedContent: this.content});
|
||||
}
|
||||
|
||||
public discardChanges(): void {
|
||||
this.discard.emit({});
|
||||
}
|
||||
|
||||
public get currentEditMode(): EditMode {
|
||||
return this.editMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text in such a way that the browser adds it to the 'undo' stack. This has different feature support
|
||||
* depending on the platform.
|
||||
*/
|
||||
private insertText(text: string, startPos: number, endPos: number): void {
|
||||
if (this.browserPlatform === 'firefox') {
|
||||
// FIXME: Ctrl-Z highlights previous text
|
||||
this.textarea.val(this.textarea.val().substr(0, startPos) +
|
||||
text +
|
||||
this.textarea.val().substr(endPos, this.textarea.val().length));
|
||||
}
|
||||
else {
|
||||
// TODO: Test other platforms (IE...)
|
||||
this.$document[0].execCommand('insertText', false, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Type representing the current editing mode.
|
||||
*/
|
||||
export type EditMode = "write" | "preview";
|
|
@ -0,0 +1,14 @@
|
|||
.markdown-input-container .glyphicon-edit {
|
||||
float: right;
|
||||
color: #ddd;
|
||||
transition: color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.markdown-input-container .glyphicon-edit:hover {
|
||||
cursor: pointer;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.markdown-input-placeholder-editable:hover {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<div class="markdown-input-container">
|
||||
<div>
|
||||
<span class="glyphicon glyphicon-edit"
|
||||
ng-if="$ctrl.canWrite && !$ctrl.isEditing"
|
||||
ng-click="$ctrl.editContent()"
|
||||
data-title="Edit {{ ::$ctrl.fieldTitle }}" data-placement="left" bs-tooltip></span>
|
||||
<div ng-if="$ctrl.content">
|
||||
<markdown-view content="$ctrl.content"></markdown-view>
|
||||
</div>
|
||||
<!-- Not set and can write -->
|
||||
<span class="markdown-input-placeholder-editable"
|
||||
ng-if="!$ctrl.content && $ctrl.canWrite"
|
||||
ng-click="$ctrl.editContent()">
|
||||
<i>Click to set {{ ::$ctrl.fieldTitle }}</i>
|
||||
</span>
|
||||
<!-- Not set and cannot write -->
|
||||
<span class="markdown-input-placeholder"
|
||||
ng-if="!$ctrl.content && !$ctrl.canWrite">
|
||||
<i>No {{ ::$ctrl.fieldTitle }} has been set</i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Inline editor -->
|
||||
<div ng-if="$ctrl.isEditing" style="margin-top: 20px;">
|
||||
<markdown-editor content="$ctrl.content"
|
||||
(save)="$ctrl.saveContent($event)"
|
||||
(discard)="$ctrl.discardContent($event)"></markdown-editor>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
import { MarkdownInputComponent } from './markdown-input.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownInputComponent", () => {
|
||||
var component: MarkdownInputComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new MarkdownInputComponent();
|
||||
});
|
||||
|
||||
describe("editContent", () => {
|
||||
|
||||
});
|
||||
|
||||
describe("saveContent", () => {
|
||||
var editedContent: string;
|
||||
|
||||
it("emits output event with changed content", (done) => {
|
||||
editedContent = "# Some markdown here";
|
||||
component.contentChanged.subscribe((event: {content: string}) => {
|
||||
expect(event.content).toEqual(editedContent);
|
||||
done();
|
||||
});
|
||||
|
||||
component.saveContent({editedContent: editedContent});
|
||||
});
|
||||
});
|
||||
|
||||
describe("discardContent", () => {
|
||||
|
||||
});
|
||||
});
|
32
static/js/directives/ui/markdown/markdown-input.component.ts
Normal file
32
static/js/directives/ui/markdown/markdown-input.component.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { Component, Input, Output, EventEmitter } from 'ng-metadata/core';
|
||||
import './markdown-input.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Displays editable Markdown content.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-input',
|
||||
templateUrl: '/static/js/directives/ui/markdown/markdown-input.component.html'
|
||||
})
|
||||
export class MarkdownInputComponent {
|
||||
|
||||
@Input('<') public content: string;
|
||||
@Input('<') public canWrite: boolean;
|
||||
@Input('@') public fieldTitle: string;
|
||||
@Output() public contentChanged: EventEmitter<{content: string}> = new EventEmitter();
|
||||
private isEditing: boolean = false;
|
||||
|
||||
public editContent(): void {
|
||||
this.isEditing = true;
|
||||
}
|
||||
|
||||
public saveContent(event: {editedContent: string}): void {
|
||||
this.contentChanged.emit({content: event.editedContent});
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
public discardContent(event: any): void {
|
||||
this.isEditing = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.markdown-toolbar-element .dropdown-menu li > * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.markdown-toolbar-element .dropdown-menu li:hover {
|
||||
cursor: pointer;
|
||||
background-color: #e6e6e6;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<div class="markdown-toolbar-element">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
data-title="Add header" data-container="body" bs-tooltip>
|
||||
<span class="glyphicon glyphicon-text-size"></span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading1'})"><h2>Heading</h2></li>
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading2'})"><h3>Heading</h3></li>
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading3'})"><h4>Heading</h4></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Bold" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'bold'})">
|
||||
<span class="glyphicon glyphicon-bold"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Italics" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'italics'})">
|
||||
<span class="glyphicon glyphicon-italic"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Block quote" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'quote'})">
|
||||
<i class="fa fa-quote-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Code snippet" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'code'})">
|
||||
<span class="glyphicon glyphicon-menu-left" style="margin-right: -6px;"></span>
|
||||
<span class="glyphicon glyphicon-menu-right"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="URL" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'link'})">
|
||||
<span class="glyphicon glyphicon-link"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Bulleted list" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'bulleted-list'})">
|
||||
<span class="glyphicon glyphicon-list"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Numbered list" data-container="body" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'numbered-list'})">
|
||||
<i class="fa fa-list-ol" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
import { MarkdownToolbarComponent } from './markdown-toolbar.component';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
|
||||
|
||||
describe("MarkdownToolbarComponent", () => {
|
||||
var component: MarkdownToolbarComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new MarkdownToolbarComponent();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { Component, Input, Output, EventEmitter } from 'ng-metadata/core';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
import './markdown-toolbar.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Toolbar containing Markdown symbol shortcuts.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-toolbar',
|
||||
templateUrl: '/static/js/directives/ui/markdown/markdown-toolbar.component.html'
|
||||
})
|
||||
export class MarkdownToolbarComponent {
|
||||
|
||||
@Input('<') public allowUndo: boolean = true;
|
||||
@Output() public insertSymbol: EventEmitter<{symbol: MarkdownSymbol}> = new EventEmitter();
|
||||
}
|
11
static/js/directives/ui/markdown/markdown-view.component.css
Normal file
11
static/js/directives/ui/markdown/markdown-view.component.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.markdown-view-content {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.markdown-view-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code * {
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<div class="markdown-view-content"
|
||||
ng-bind-html="$ctrl.convertedHTML"></div>
|
|
@ -0,0 +1,81 @@
|
|||
import { MarkdownViewComponent } from './markdown-view.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import { Converter, ConverterOptions } from 'showdown';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownViewComponent", () => {
|
||||
var component: MarkdownViewComponent;
|
||||
var markdownConverterMock: Mock<Converter>;
|
||||
var $sceMock: Mock<ng.ISCEService>;
|
||||
var $sanitizeMock: ng.sanitize.ISanitizeService;
|
||||
|
||||
beforeEach(() => {
|
||||
markdownConverterMock = new Mock<Converter>();
|
||||
$sceMock = new Mock<ng.ISCEService>();
|
||||
$sanitizeMock = jasmine.createSpy('$sanitizeSpy').and.callFake((html: string) => html);
|
||||
component = new MarkdownViewComponent((options: ConverterOptions) => markdownConverterMock.Object,
|
||||
$sceMock.Object,
|
||||
$sanitizeMock);
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
var markdown: string;
|
||||
var expectedPlaceholder: string;
|
||||
var markdownChars: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
changes = {};
|
||||
markdown = `## Heading\n Code line\n\n- Item\n> Quote\`code snippet\`\n\nThis is my project!`;
|
||||
expectedPlaceholder = `<p style="visibility:hidden">placeholder</p>`;
|
||||
markdownChars = ['#', '-', '>', '`'];
|
||||
markdownConverterMock.setup(mock => mock.makeHtml).is((text) => text);
|
||||
$sceMock.setup(mock => mock.trustAsHtml).is((html) => html);
|
||||
});
|
||||
|
||||
it("calls markdown converter to convert content to HTML when content is changed", () => {
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
|
||||
it("only converts first line of content to HTML if flag is set when content is changed", () => {
|
||||
component.firstLineOnly = true;
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
const expectedHtml: string = markdown.split('\n')
|
||||
.filter(line => line.indexOf(' ') != 0)
|
||||
.filter(line => line.trim().length != 0)
|
||||
.filter(line => markdownChars.indexOf(line.trim()[0]) == -1)[0];
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(expectedHtml);
|
||||
});
|
||||
|
||||
it("sets converted HTML to be a placeholder if flag is set and content is empty", () => {
|
||||
component.placeholderNeeded = true;
|
||||
changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml)).not.toHaveBeenCalled();
|
||||
expect((<Spy>$sceMock.Object.trustAsHtml).calls.argsFor(0)[0]).toEqual(expectedPlaceholder);
|
||||
});
|
||||
|
||||
it("sets converted HTML to empty string if placeholder flag is false and content is empty", () => {
|
||||
changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
|
||||
it("calls $sanitize service to sanitize changed HTML content", () => {
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>$sanitizeMock).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
});
|
||||
});
|
51
static/js/directives/ui/markdown/markdown-view.component.ts
Normal file
51
static/js/directives/ui/markdown/markdown-view.component.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { Component, Input, Inject, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
import { Converter, ConverterOptions } from 'showdown';
|
||||
import 'showdown-highlightjs-extension';
|
||||
import 'highlightjs/styles/vs.css';
|
||||
import './markdown-view.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Renders Markdown content to HTML.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-view',
|
||||
templateUrl: '/static/js/directives/ui/markdown/markdown-view.component.html'
|
||||
})
|
||||
export class MarkdownViewComponent implements OnChanges {
|
||||
|
||||
@Input('<') public content: string;
|
||||
@Input('<') public firstLineOnly: boolean = false;
|
||||
@Input('<') public placeholderNeeded: boolean = false;
|
||||
private convertedHTML: string = '';
|
||||
private readonly placeholder: string = `<p style="visibility:hidden">placeholder</p>`;
|
||||
private readonly markdownChars: string[] = ['#', '-', '>', '`'];
|
||||
private markdownConverter: Converter;
|
||||
|
||||
constructor(@Inject('markdownConverterFactory') private makeConverter: (options?: ConverterOptions) => Converter,
|
||||
@Inject('$sce') private $sce: ng.ISCEService,
|
||||
@Inject('$sanitize') private $sanitize: ng.sanitize.ISanitizeService) {
|
||||
this.markdownConverter = makeConverter({extensions: ['highlightjs']});
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['content']) {
|
||||
if (!changes['content'].currentValue && this.placeholderNeeded) {
|
||||
this.convertedHTML = this.$sce.trustAsHtml(this.placeholder);
|
||||
} else if (this.firstLineOnly) {
|
||||
const firstLine: string = changes['content'].currentValue.split('\n')
|
||||
// Skip code lines
|
||||
.filter(line => line.indexOf(' ') != 0)
|
||||
// Skip empty lines
|
||||
.filter(line => line.trim().length != 0)
|
||||
// Skip control lines
|
||||
.filter(line => this.markdownChars.indexOf(line.trim()[0]) == -1)[0];
|
||||
|
||||
this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(firstLine));
|
||||
} else {
|
||||
this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(changes['content'].currentValue));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,8 +39,11 @@
|
|||
<span class="avatar" data="result.namespace.avatar" size="16"></span>
|
||||
<span class="result-name">{{ result.namespace.name }}/{{ result.name }}</span>
|
||||
<div class="result-description" ng-if="result.description">
|
||||
<div class="description markdown-view" content="result.description"
|
||||
first-line-only="true" placeholder-needed="false"></div>
|
||||
<div class="description">
|
||||
<markdown-view content="result.description"
|
||||
first-line-only="true"
|
||||
placeholder-needed="false"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -48,6 +48,10 @@
|
|||
$scope.startBuildCallback = startBuild;
|
||||
};
|
||||
|
||||
$scope.updateDescription = function(content) {
|
||||
$scope.repo.description = content;
|
||||
};
|
||||
|
||||
$scope.createNewRepo = function() {
|
||||
$scope.creating = true;
|
||||
var repo = $scope.repo;
|
||||
|
|
|
@ -32,6 +32,12 @@ import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.
|
|||
import { DataFileServiceImpl } from './services/datafile/datafile.service.impl';
|
||||
import { UtilServiceImpl } from './services/util/util.service.impl';
|
||||
import { QuayRequireDirective } from './directives/structural/quay-require/quay-require.directive';
|
||||
import { MarkdownInputComponent } from './directives/ui/markdown/markdown-input.component';
|
||||
import { MarkdownViewComponent } from './directives/ui/markdown/markdown-view.component';
|
||||
import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-toolbar.component';
|
||||
import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component';
|
||||
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
|
||||
import { Converter, ConverterOptions } from 'showdown';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -60,6 +66,10 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay-
|
|||
RepositorySigningConfigComponent,
|
||||
TimeMachineSettingsComponent,
|
||||
DurationInputComponent,
|
||||
MarkdownInputComponent,
|
||||
MarkdownViewComponent,
|
||||
MarkdownToolbarComponent,
|
||||
MarkdownEditorComponent,
|
||||
SearchBoxComponent,
|
||||
TypeaheadDirective,
|
||||
CorTabPanelComponent,
|
||||
|
@ -74,8 +84,9 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay-
|
|||
AvatarServiceImpl,
|
||||
DockerfileServiceImpl,
|
||||
DataFileServiceImpl,
|
||||
UtilServiceImpl,
|
||||
{provide: 'fileReaderFactory', useValue: () => new FileReader()},
|
||||
{provide: 'markdownConverterFactory', useValue: (options?: ConverterOptions) => new Converter(options)},
|
||||
{provide: 'BrowserPlatform', useValue: browserPlatform},
|
||||
{provide: 'CorTabCurrentHandlerFactory', useValue: CorTabCurrentHandlerFactory},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -104,6 +104,7 @@ export type Trigger = {
|
|||
service: any;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Represents an apostille signature document, with extra expiration information.
|
||||
*/
|
||||
|
@ -118,6 +119,7 @@ export type ApostilleSignatureDocument = {
|
|||
error: boolean
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An apostille document containing signatures for a tag.
|
||||
*/
|
||||
|
@ -127,4 +129,20 @@ export type ApostilleTagDocument = {
|
|||
|
||||
// The hashes for the tag.
|
||||
hashes: {string: string}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A type representing a Markdown symbol.
|
||||
*/
|
||||
export type MarkdownSymbol = 'heading1'
|
||||
| 'heading2'
|
||||
| 'heading3'
|
||||
| 'bold'
|
||||
| 'italics'
|
||||
| 'bulleted-list'
|
||||
| 'numbered-list'
|
||||
| 'quote'
|
||||
| 'code'
|
||||
| 'link'
|
||||
| 'code';
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
A javascript port of Markdown, as used on Stack Overflow
|
||||
and the rest of Stack Exchange network.
|
||||
|
||||
Largely based on showdown.js by John Fraser (Attacklab).
|
||||
|
||||
Original Markdown Copyright (c) 2004-2005 John Gruber
|
||||
<http://daringfireball.net/projects/markdown/>
|
||||
|
||||
|
||||
Original Showdown code copyright (c) 2007 John Fraser
|
||||
|
||||
Modifications and bugfixes (c) 2009 Dana Robinson
|
||||
Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
File diff suppressed because it is too large
Load diff
Binary file not shown.
Before Width: | Height: | Size: 50 KiB |
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
File diff suppressed because it is too large
Load diff
|
@ -1,80 +0,0 @@
|
|||
|
||||
.wmd-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wmd-input {
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wmd-preview {
|
||||
.well;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
-webkit-box-sizing:border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.wmd-panel .btn-toolbar {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.icon-link,
|
||||
.icon-blockquote,
|
||||
.icon-code,
|
||||
.icon-bullet-list,
|
||||
.icon-list,
|
||||
.icon-header,
|
||||
.icon-hr-line,
|
||||
.icon-undo {
|
||||
background-image: url(Markdown.Editor.Icons.png);
|
||||
}
|
||||
.icon-link { background-position: 0 0; }
|
||||
.icon-blockquote { background-position: -24px 0; }
|
||||
.icon-code { background-position: -48px 0; }
|
||||
.icon-bullet-list { background-position: -72px 0; }
|
||||
.icon-list { background-position: -96px 0; }
|
||||
.icon-header { background-position: -120px 0; }
|
||||
.icon-hr-line { background-position: -144px 0; }
|
||||
.icon-undo { background-position: -168px 0; }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.wmd-prompt-background
|
||||
{
|
||||
background-color: Black;
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog
|
||||
{
|
||||
border: 1px solid #999999;
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog > div {
|
||||
font-size: 0.8em;
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.wmd-prompt-dialog > form > input[type="text"] {
|
||||
border: 1px solid #999999;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.wmd-prompt-dialog > form > input[type="button"]{
|
||||
border: 1px solid #888888;
|
||||
font-family: trebuchet MS, helvetica, sans-serif;
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
(function () {
|
||||
var output, Converter;
|
||||
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
|
||||
output = exports;
|
||||
Converter = require("./Markdown.Converter").Converter;
|
||||
} else {
|
||||
output = window.Markdown;
|
||||
Converter = output.Converter;
|
||||
}
|
||||
|
||||
output.getSanitizingConverter = function () {
|
||||
var converter = new Converter();
|
||||
converter.hooks.chain("postConversion", sanitizeHtml);
|
||||
converter.hooks.chain("postConversion", balanceTags);
|
||||
return converter;
|
||||
}
|
||||
|
||||
function sanitizeHtml(html) {
|
||||
return html.replace(/<[^>]*>?/gi, sanitizeTag);
|
||||
}
|
||||
|
||||
// (tags that can be opened/closed) | (tags that stand alone)
|
||||
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
|
||||
// <a href="url..." optional title>|</a>
|
||||
var a_white = /^(<a\shref="(https?:(\/\/|\/)|ftp:(\/\/|\/)|mailto:|magnet:)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
|
||||
|
||||
// <img src="url..." optional width optional height optional alt optional title
|
||||
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
|
||||
|
||||
// <pre optional class="prettyprint linenums">|</pre> for twitter bootstrap
|
||||
var pre_white = /^(<pre(\sclass="prettyprint linenums")?>|<\/pre>)$/i;
|
||||
|
||||
function sanitizeTag(tag) {
|
||||
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white))
|
||||
return tag;
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// attempt to balance HTML tags in the html string
|
||||
/// by removing any unmatched opening or closing tags
|
||||
/// IMPORTANT: we *assume* HTML has *already* been
|
||||
/// sanitized and is safe/sane before balancing!
|
||||
///
|
||||
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
|
||||
/// </summary>
|
||||
function balanceTags(html) {
|
||||
|
||||
if (html == "")
|
||||
return "";
|
||||
|
||||
var re = /<\/?\w+[^>]*(\s|$|>)/g;
|
||||
// convert everything to lower case; this makes
|
||||
// our case insensitive comparisons easier
|
||||
var tags = html.toLowerCase().match(re);
|
||||
|
||||
// no HTML tags present? nothing to do; exit now
|
||||
var tagcount = (tags || []).length;
|
||||
if (tagcount == 0)
|
||||
return html;
|
||||
|
||||
var tagname, tag;
|
||||
var ignoredtags = "<p><img><br><li><hr>";
|
||||
var match;
|
||||
var tagpaired = [];
|
||||
var tagremove = [];
|
||||
var needsRemoval = false;
|
||||
|
||||
// loop through matched tags in forward order
|
||||
for (var ctag = 0; ctag < tagcount; ctag++) {
|
||||
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
|
||||
// skip any already paired tags
|
||||
// and skip tags in our ignore list; assume they're self-closed
|
||||
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
|
||||
continue;
|
||||
|
||||
tag = tags[ctag];
|
||||
match = -1;
|
||||
|
||||
if (!/^<\//.test(tag)) {
|
||||
// this is an opening tag
|
||||
// search forwards (next tags), look for closing tags
|
||||
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
|
||||
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
|
||||
match = ntag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (match == -1)
|
||||
needsRemoval = tagremove[ctag] = true; // mark for removal
|
||||
else
|
||||
tagpaired[match] = true; // mark paired
|
||||
}
|
||||
|
||||
if (!needsRemoval)
|
||||
return html;
|
||||
|
||||
// delete all orphaned tags from the string
|
||||
|
||||
var ctag = 0;
|
||||
html = html.replace(re, function (match) {
|
||||
var res = tagremove[ctag] ? "" : match;
|
||||
ctag++;
|
||||
return res;
|
||||
});
|
||||
return html;
|
||||
}
|
||||
})();
|
|
@ -63,8 +63,12 @@
|
|||
<div class="section">
|
||||
<div class="section-title">Repository Description</div>
|
||||
<br>
|
||||
<div class="description markdown-input" content="repo.description" can-write="true"
|
||||
field-title="'repository description'"></div>
|
||||
<div class="description">
|
||||
<markdown-input content="repo.description"
|
||||
can-write="true"
|
||||
(content-changed)="updateDescription($event.content)"
|
||||
field-title="repository description"></markdown-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
<a href="{{ result.href }}">{{ result.namespace.name }}/{{ result.name }}</a>
|
||||
</h4>
|
||||
<p class="description">
|
||||
<span class="markdown-view" content="result.description" first-line-only="true"></span>
|
||||
<markdown-view content="result.description"
|
||||
first-line-only="true"></markdown-view>
|
||||
</p>
|
||||
<p class="result-info-bar">
|
||||
<span class="info-span">Last Modified: <span am-time-ago="result.last_modified * 1000"></span></span>
|
||||
|
|
|
@ -120,7 +120,8 @@
|
|||
<cor-tab-pane id="change-log">
|
||||
<h3 style="margin-top: 0px;">Change Log</h3>
|
||||
<div class="cor-loader" ng-if="!changeLog"></div>
|
||||
<div class="markdown-view" content="changeLog.log" ng-if="changeLog"></div>
|
||||
<markdown-view ng-if="changeLog"
|
||||
content="changeLog.log" ></markdown-view>
|
||||
</cor-tab-pane> <!-- /change-log tab-->
|
||||
|
||||
<!-- Organizations tab -->
|
||||
|
|
|
@ -59,10 +59,12 @@
|
|||
<!-- Description -->
|
||||
<div class="section-header">Team Description</div>
|
||||
<div class="team-view-header">
|
||||
<div class="description markdown-input" content="team.description"
|
||||
can-write="organization.is_admin"
|
||||
content-changed="updateForDescription"
|
||||
field-title="'team description'"></div>
|
||||
<div class="description">
|
||||
<markdown-input content="team.description"
|
||||
can-write="organization.is_admin"
|
||||
(content-changed)="updateForDescription($event.content)"
|
||||
field-title="team description"></markdown-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Members -->
|
||||
|
|
|
@ -16,7 +16,7 @@ var config = {
|
|||
// Use window.angular to maintain compatibility with non-Webpack components
|
||||
externals: {
|
||||
angular: "angular",
|
||||
jquery: "$",
|
||||
jquery: "$"
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -28,13 +28,12 @@ var config = {
|
|||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
test: /\.s?css$/,
|
||||
use: [
|
||||
"style-loader",
|
||||
"css-loader",
|
||||
"sass-loader",
|
||||
],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
]
|
||||
},
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -50,6 +50,10 @@
|
|||
version "0.14.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-0.14.39.tgz#11cb715768da5f7605aa2030a5dc63e77a137eb5"
|
||||
|
||||
"@types/showdown@^1.4.32":
|
||||
version "1.4.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-1.4.32.tgz#bb0b32dbafee23ae9575df30b227e4fc2f0cd45b"
|
||||
|
||||
abbrev@1, abbrev@1.0.x:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
|
||||
|
@ -1634,6 +1638,10 @@ hawk@~3.1.3:
|
|||
hoek "2.x.x"
|
||||
sntp "1.x.x"
|
||||
|
||||
highlightjs@^9.8.0:
|
||||
version "9.10.0"
|
||||
resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.10.0.tgz#fca9b78ddaa3b1abca89d6c3ee105ad270a80190"
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.0.tgz#3db471f45aae4a994a0688322171f51b8b91bee5"
|
||||
|
@ -3558,6 +3566,18 @@ sha.js@^2.3.6:
|
|||
dependencies:
|
||||
inherits "^2.0.1"
|
||||
|
||||
showdown-highlightjs-extension@^0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/showdown-highlightjs-extension/-/showdown-highlightjs-extension-0.1.2.tgz#0fc90190283c1ae03fc4cccce3f1be6a5a58e4ce"
|
||||
dependencies:
|
||||
highlightjs "^9.8.0"
|
||||
|
||||
showdown@^1.6.4:
|
||||
version "1.6.4"
|
||||
resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.6.4.tgz#056bbb654ecdb8d8643ae12d6d597893ccaf46c6"
|
||||
dependencies:
|
||||
yargs "^6.6.0"
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
|
@ -4347,7 +4367,7 @@ yargs@^4.7.1:
|
|||
y18n "^3.2.1"
|
||||
yargs-parser "^2.4.1"
|
||||
|
||||
yargs@^6.0.0:
|
||||
yargs@^6.0.0, yargs@^6.6.0:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
|
||||
dependencies:
|
||||
|
|
Reference in a new issue