Added share by link feature
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 656 KiB |
@@ -254,8 +254,7 @@
|
||||
</ng-container>
|
||||
|
||||
<div style="flex:1"></div>
|
||||
<button mat-icon-button [matMenuTriggerFor]="appMenu" [matMenuTriggerData]="{item:item, idx:j}"
|
||||
>
|
||||
<button mat-icon-button [matMenuTriggerFor]="appMenu" [matMenuTriggerData]="{item:item, idx:j}" class="more-button">
|
||||
<mat-icon svgIcon="more"></mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
@@ -343,7 +342,8 @@
|
||||
<mat-icon>image</mat-icon>
|
||||
</button>
|
||||
|
||||
<app-export [path]="rawPath" [name]="pathName || '' "></app-export>
|
||||
<app-export [path]="rawPath" [name]="pathName || '' " style="margin-right: 8px"></app-export>
|
||||
<app-share [path]="rawPath"></app-share>
|
||||
</div>
|
||||
<div class="scene-bottom-actions">
|
||||
<button
|
||||
@@ -434,6 +434,8 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-import (importPath)="openPath($event, '')"></app-import>
|
||||
|
||||
<mat-menu #appMenu="matMenu" xPosition="before">
|
||||
<ng-template matMenuContent let-item="item" let-idx="idx">
|
||||
<button mat-menu-item [matMenuTriggerFor]="appCreate" [matMenuTriggerData]="{item:item, idx:idx}">
|
||||
|
||||
@@ -10,6 +10,10 @@ import { UploadImageComponent } from './upload-image/upload-image.component';
|
||||
import { ConfigService } from './config.service';
|
||||
import { browserComputePathBoundingBox } from './svg-bbox';
|
||||
|
||||
export const kDefaultPath = `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 `
|
||||
+ `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 `
|
||||
+ `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`;
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
@@ -32,10 +36,7 @@ export class AppComponent implements AfterViewInit {
|
||||
controlPoints: SvgControlPoint[] = [];
|
||||
|
||||
// Raw path:
|
||||
_rawPath = this.storage.getPath()?.path
|
||||
|| `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 `
|
||||
+ `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 `
|
||||
+ `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`;
|
||||
_rawPath = this.storage.getPath()?.path || kDefaultPath;
|
||||
pathName: string = '';
|
||||
invalidSyntax = false;
|
||||
|
||||
@@ -105,10 +106,16 @@ export class AppComponent implements AfterViewInit {
|
||||
} else if (!$event.metaKey && !$event.ctrlKey && /^[mlvhcsqtaz]$/i.test($event.key)) {
|
||||
const isLower = $event.key === $event.key.toLowerCase();
|
||||
const key = $event.key.toUpperCase();
|
||||
if (isLower && this.focusedItem && this.canInsertAfter(this.focusedItem, key)) {
|
||||
this.insert(key, this.focusedItem, false);
|
||||
$event.preventDefault();
|
||||
if (isLower) {
|
||||
// Item insertion
|
||||
const lastItem = this.parsedPath.path.length ? this.parsedPath.path[this.parsedPath.path.length - 1] : null;
|
||||
const prevItem = this.focusedItem || lastItem;
|
||||
if(this.canInsertAfter(prevItem, key)) {
|
||||
this.insert(key, prevItem, false);
|
||||
$event.preventDefault();
|
||||
}
|
||||
} else if (!isLower && this.focusedItem && this.canConvert(this.focusedItem, key)) {
|
||||
// Item convertion
|
||||
this.insert(key, this.focusedItem, true);
|
||||
$event.preventDefault();
|
||||
}
|
||||
@@ -221,11 +228,13 @@ export class AppComponent implements AfterViewInit {
|
||||
this.strokeWidth = this.cfg.viewPortWidth / this.canvasWidth;
|
||||
}
|
||||
|
||||
insert(type: string, after: SvgItem, convert: boolean) {
|
||||
insert(type: string, after: SvgItem | null, convert: boolean) {
|
||||
if (convert) {
|
||||
this.focusedItem =
|
||||
this.parsedPath.changeType(after, after.relative ? type.toLowerCase() : type);
|
||||
this.afterModelChange();
|
||||
if(after) {
|
||||
this.focusedItem =
|
||||
this.parsedPath.changeType(after, after.relative ? type.toLowerCase() : type);
|
||||
this.afterModelChange();
|
||||
}
|
||||
} else {
|
||||
this.draggedIsNew = true;
|
||||
const pts = this.targetPoints;
|
||||
@@ -263,7 +272,7 @@ export class AppComponent implements AfterViewInit {
|
||||
newItem = SvgItem.Make([type]);
|
||||
}
|
||||
if(newItem) {
|
||||
this.parsedPath.insert(newItem, after);
|
||||
this.parsedPath.insert(newItem, after ?? undefined);
|
||||
}
|
||||
}
|
||||
this.setHistoryDisabled(true);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -9,27 +11,29 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import {MatSnackBarModule} from '@angular/material/snack-bar';
|
||||
import { MatTooltipModule, MAT_TOOLTIP_SCROLL_STRATEGY } from '@angular/material/tooltip';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { Overlay, ScrollStrategy } from '@angular/cdk/overlay';
|
||||
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { ExpandableComponent } from './expandable/expandable.component';
|
||||
import { CanvasComponent } from './canvas/canvas.component';
|
||||
import { OpenComponent, OpenDialogComponent } from './open/open.component';
|
||||
import { SaveComponent, SaveDialogComponent } from './save/save.component';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
|
||||
import { Overlay, ScrollStrategy } from '@angular/cdk/overlay';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppComponent } from './app.component';
|
||||
import { FormatterDirective } from './formatter/formatter.directive';
|
||||
import { KeyboardNavigableDirective } from './keyboard-navigable/keyboard-navigable.directive';
|
||||
import { ExpandableComponent } from './expandable/expandable.component';
|
||||
import { CanvasComponent } from './canvas/canvas.component';
|
||||
import { PathPreviewComponent } from './path-preview/path-preview.component';
|
||||
import { OpenComponent, OpenDialogComponent } from './open/open.component';
|
||||
import { SaveComponent, SaveDialogComponent } from './save/save.component';
|
||||
import { ExportComponent, ExportDialogComponent } from './export/export.component';
|
||||
import { UploadImageComponent, UploadImageDialogComponent } from './upload-image/upload-image.component';
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { ImportComponent, ImportDialogComponent } from './import/import.component';
|
||||
import { ShareComponent, ShareDialogComponent, ShareDialogSnackbarComponent } from './share/share.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -44,8 +48,14 @@ import { environment } from '../environments/environment';
|
||||
ExportDialogComponent,
|
||||
UploadImageComponent,
|
||||
UploadImageDialogComponent,
|
||||
ImportComponent,
|
||||
ImportDialogComponent,
|
||||
ShareComponent,
|
||||
ShareDialogComponent,
|
||||
ShareDialogSnackbarComponent,
|
||||
FormatterDirective,
|
||||
KeyboardNavigableDirective
|
||||
KeyboardNavigableDirective,
|
||||
PathPreviewComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -62,6 +72,7 @@ import { environment } from '../environments/environment';
|
||||
MatTableModule,
|
||||
MatSortModule,
|
||||
MatSliderModule,
|
||||
MatSnackBarModule,
|
||||
BrowserAnimationsModule,
|
||||
ScrollingModule,
|
||||
ServiceWorkerModule.register('ngsw-worker.js', {
|
||||
|
||||
@@ -63,21 +63,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview">
|
||||
<svg [attr.viewBox]="x+' '+y+' '+width+' '+height"
|
||||
width="300" height="300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<pattern id="preview-pattern" x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse" [attr.patternTransform]="'scale('+patternScale(400, 400)+')'">
|
||||
<rect x="0" y="0" width="16" height="16" fill="white"></rect>
|
||||
<rect x="0" y="0" width="8" height="8" fill="#cccccc"></rect>
|
||||
<rect x="8" y="8" width="8" height="8" fill="#cccccc"></rect>
|
||||
</pattern>
|
||||
<clipPath id="preview-clippath">
|
||||
<rect [attr.x]="x" [attr.y]="y" [attr.width]="width" [attr.height]="height"></rect>
|
||||
</clipPath>
|
||||
<rect [attr.x]="x" [attr.y]="y" [attr.width]="width" [attr.height]="height" fill="url(#preview-pattern)" ></rect>
|
||||
<path [attr.d]="data.path" [attr.fill]="cfg.fill?cfg.fillColor:'none'" [attr.stroke-width]="cfg.strokeWidth" [attr.stroke]="cfg.stroke?cfg.strokeColor:'none'" clip-path="url(#preview-clippath)"></path>
|
||||
</svg>
|
||||
|
||||
<app-path-preview
|
||||
[x]="x"
|
||||
[y]="y"
|
||||
[width]="width"
|
||||
[height]="height"
|
||||
[path]="data.path"
|
||||
[fillColor]="cfg.fill?cfg.fillColor:'none'"
|
||||
[strokeColor]="cfg.stroke?cfg.strokeColor:'none'"
|
||||
[strokeWidth]="cfg.strokeWidth"
|
||||
></app-path-preview>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ mat-form-field {
|
||||
}
|
||||
.preview {
|
||||
width:324px;
|
||||
svg {
|
||||
app-path-preview {
|
||||
margin:0 0 0 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,6 @@ export class ExportDialogComponent {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
patternScale(containterWidth: number, containerHeight: number): number {
|
||||
return Math.max(this.width / containterWidth, this.height / containerHeight);
|
||||
}
|
||||
|
||||
refreshViewbox() {
|
||||
const p = new Svg(this.data.path);
|
||||
const locs = p.targetLocations();
|
||||
|
||||
20
src/app/import/import-dialog.component.html
Normal file
20
src/app/import/import-dialog.component.html
Normal file
@@ -0,0 +1,20 @@
|
||||
<h1 mat-dialog-title>Import shared path</h1>
|
||||
<div mat-dialog-content>
|
||||
<div style="display: flex;">
|
||||
<app-path-preview [path]="data.path ?? ''"></app-path-preview>
|
||||
<div style="padding:0 32px;">
|
||||
<p>
|
||||
Press <b>Import</b> to import this SVG path in the editor.
|
||||
</p>
|
||||
<p>
|
||||
Please note it will erase any unsaved change.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-raised-button (click)="onCancel()" color="basic">Discard</button>
|
||||
<button mat-raised-button (click)="onConfirm()" color="primary" [disabled]="false">
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
28
src/app/import/import.component.spec.ts
Normal file
28
src/app/import/import.component.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { ImportComponent } from './import.component';
|
||||
|
||||
describe('ImportComponent', () => {
|
||||
let component: ImportComponent;
|
||||
let fixture: ComponentFixture<ImportComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ MatDialogModule, MatIconModule ],
|
||||
declarations: [ ImportComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ImportComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
89
src/app/import/import.component.ts
Normal file
89
src/app/import/import.component.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { kDefaultPath } from '../app.component';
|
||||
import { StorageService } from '../storage.service';
|
||||
import { Svg } from '../svg';
|
||||
|
||||
export class DialogData {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-import-dialog',
|
||||
templateUrl: 'import-dialog.component.html'
|
||||
})
|
||||
export class ImportDialogComponent {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ImportDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData
|
||||
) {
|
||||
}
|
||||
|
||||
onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-import',
|
||||
template: ''
|
||||
})
|
||||
export class ImportComponent implements OnInit {
|
||||
private urlPath?: string;
|
||||
@Output() importPath = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
public storageService: StorageService,
|
||||
) {
|
||||
this.urlPath = this.readPath();
|
||||
}
|
||||
|
||||
private readPath(): string {
|
||||
const fragment = decodeURIComponent(window.location.hash.slice(1));
|
||||
const check = /^P=[mMlLvVhHcCsSqQtTaAzZ0-9\-e._]+$/;
|
||||
if(check.test(fragment)) {
|
||||
const path = fragment.slice(2).replace(/_/g, ' ');
|
||||
try {
|
||||
const _ = new Svg(path);
|
||||
return path;
|
||||
} catch (e) {}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const openedPath = this.storageService.getPath();
|
||||
const unsavedChanges = openedPath && openedPath.path !== kDefaultPath;
|
||||
if(this.urlPath && this.urlPath !== openedPath?.path) {
|
||||
if(unsavedChanges) {
|
||||
this.openDialog();
|
||||
} else {
|
||||
this.finalize(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(ImportDialogComponent, {
|
||||
width: '800px',
|
||||
panelClass: 'dialog',
|
||||
autoFocus: false,
|
||||
data: {path: this.urlPath}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
this.finalize(result);
|
||||
});
|
||||
}
|
||||
|
||||
private finalize(result: boolean): void {
|
||||
if(result) {
|
||||
this.importPath.emit(this.urlPath);
|
||||
}
|
||||
window.location.hash = '';
|
||||
}
|
||||
}
|
||||
15
src/app/path-preview/path-preview.component.html
Normal file
15
src/app/path-preview/path-preview.component.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg [attr.viewBox]="x+' '+y+' '+width+' '+height"
|
||||
width="300" height="300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<pattern id="preview-pattern" x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse" [attr.patternTransform]="'scale('+patternScale(400, 400)+')'">
|
||||
<rect x="0" y="0" width="16" height="16" fill="white"></rect>
|
||||
<rect x="0" y="0" width="8" height="8" fill="#cccccc"></rect>
|
||||
<rect x="8" y="8" width="8" height="8" fill="#cccccc"></rect>
|
||||
</pattern>
|
||||
<clipPath id="preview-clippath">
|
||||
<rect [attr.x]="x" [attr.y]="y" [attr.width]="width" [attr.height]="height"></rect>
|
||||
</clipPath>
|
||||
<rect [attr.x]="x" [attr.y]="y" [attr.width]="width" [attr.height]="height" fill="url(#preview-pattern)" ></rect>
|
||||
<path [attr.d]="path" [attr.fill]="fillColor||'none'" [attr.stroke-width]="strokeWidth" [attr.stroke]="strokeColor||'none'" clip-path="url(#preview-clippath)"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 912 B |
23
src/app/path-preview/path-preview.component.spec.ts
Normal file
23
src/app/path-preview/path-preview.component.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PathPreviewComponent } from './path-preview.component';
|
||||
|
||||
describe('PathPreviewComponent', () => {
|
||||
let component: PathPreviewComponent;
|
||||
let fixture: ComponentFixture<PathPreviewComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PathPreviewComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PathPreviewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
35
src/app/path-preview/path-preview.component.ts
Normal file
35
src/app/path-preview/path-preview.component.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { browserComputePathBoundingBox } from '../svg-bbox';
|
||||
|
||||
@Component({
|
||||
selector: 'app-path-preview',
|
||||
templateUrl: './path-preview.component.html'
|
||||
})
|
||||
export class PathPreviewComponent implements OnInit {
|
||||
@Input() x?: number;
|
||||
@Input() y?: number;
|
||||
@Input() width?: number;
|
||||
@Input() height?: number;
|
||||
|
||||
@Input() fillColor?: string = '#000000';
|
||||
@Input() strokeColor?: string;
|
||||
@Input() strokeWidth?: number;
|
||||
@Input() path = '';
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if(this.x === undefined || this.y === undefined || this.width === undefined || this.height === undefined) {
|
||||
const bbox = browserComputePathBoundingBox(this.path);
|
||||
this.x = bbox.x;
|
||||
this.y = bbox.y;
|
||||
this.width = bbox.width;
|
||||
this.height = bbox.height;
|
||||
}
|
||||
}
|
||||
|
||||
patternScale(containterWidth: number, containerHeight: number): number {
|
||||
return Math.max((this.width??0) / containterWidth, (this.height??0) / containerHeight);
|
||||
}
|
||||
|
||||
}
|
||||
4
src/app/share/share-dialog-snackbar.component.html
Normal file
4
src/app/share/share-dialog-snackbar.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div style="display:flex; align-items:center">
|
||||
<span class="mat-simple-snackbar" style="flex:1">Copied to clipboard</span>
|
||||
<mat-icon>content_pasted</mat-icon>
|
||||
</div>
|
||||
19
src/app/share/share-dialog.component.html
Normal file
19
src/app/share/share-dialog.component.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<h1 mat-dialog-title>Share path as URL</h1>
|
||||
<div mat-dialog-content>
|
||||
<div style="display: flex;">
|
||||
<app-path-preview [path]="data.path ?? ''"></app-path-preview>
|
||||
<div style="padding:0 32px; flex:1">
|
||||
<p>The current path can be shared using the following link:</p>
|
||||
<mat-form-field floatLabel="always" appearance="fill" style="width: 100%;">
|
||||
<mat-label>URL</mat-label>
|
||||
<input matInput readonly [ngModel]="getUrl()" #input>
|
||||
<button mat-icon-button matSuffix matTooltip="Copy to clipboard" (click)="copy()">
|
||||
<mat-icon>content_pasted</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions align="end">
|
||||
<button mat-raised-button (click)="onCancel()" color="default">Close</button>
|
||||
</div>
|
||||
8
src/app/share/share.component.html
Normal file
8
src/app/share/share.component.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<button mat-mini-fab
|
||||
color="basic"
|
||||
matTooltip="Share as link"
|
||||
matTooltipPosition="below"
|
||||
(click)="openDialog()"
|
||||
>
|
||||
<mat-icon>share</mat-icon>
|
||||
</button>
|
||||
28
src/app/share/share.component.spec.ts
Normal file
28
src/app/share/share.component.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { ShareComponent } from './share.component';
|
||||
|
||||
describe('ShareComponent', () => {
|
||||
let component: ShareComponent;
|
||||
let fixture: ComponentFixture<ShareComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ MatDialogModule, MatIconModule ],
|
||||
declarations: [ ShareComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ShareComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
84
src/app/share/share.component.ts
Normal file
84
src/app/share/share.component.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Component, Output, EventEmitter, Inject, Input, ViewChild, AfterViewInit, ElementRef } from '@angular/core';
|
||||
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { StorageService } from '../storage.service';
|
||||
|
||||
export class DialogData {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-share-snackbar',
|
||||
templateUrl: 'share-dialog-snackbar.component.html'
|
||||
})
|
||||
export class ShareDialogSnackbarComponent {}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-share-dialog',
|
||||
templateUrl: 'share-dialog.component.html'
|
||||
})
|
||||
export class ShareDialogComponent implements AfterViewInit {
|
||||
@ViewChild('input') inputField?: ElementRef;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ShareDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: DialogData,
|
||||
private snackBar: MatSnackBar
|
||||
) {
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setTimeout(() => this.selectText());
|
||||
}
|
||||
|
||||
private selectText(): void {
|
||||
const el = this.inputField?.nativeElement;
|
||||
el?.focus();
|
||||
el?.select();
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
copy(): void {
|
||||
this.selectText();
|
||||
navigator.clipboard.writeText(this.inputField?.nativeElement.value);
|
||||
this.snackBar.openFromComponent(ShareDialogSnackbarComponent, {
|
||||
horizontalPosition:'center',
|
||||
verticalPosition: 'top',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
|
||||
getUrl(): string {
|
||||
const loc = window.location;
|
||||
const fragment = this.data.path?.replace(/ +/g, '_');
|
||||
return `${loc.protocol}//${loc.host}${loc.pathname}#P=${fragment}`;
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-share',
|
||||
templateUrl: './share.component.html'
|
||||
})
|
||||
export class ShareComponent {
|
||||
@Input() path: string = '';
|
||||
@Output() importPath = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
public storageService: StorageService,
|
||||
) {
|
||||
}
|
||||
|
||||
openDialog(): void {
|
||||
const dialogRef = this.dialog.open(ShareDialogComponent, {
|
||||
width: '800px',
|
||||
panelClass: 'dialog',
|
||||
autoFocus: false,
|
||||
data: {path: this.path}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ button.mat-flat-button {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
button.mat-icon-button {
|
||||
button.mat-icon-button.more-button {
|
||||
width:20px;
|
||||
height:20px;
|
||||
line-height: 20px !important;
|
||||
|
||||
Reference in New Issue
Block a user