Fix wiki-toolbar previewing

pull/6346/head
Oliver Günther 6 years ago
parent 379d061a6b
commit 3eac7eeb51
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 2
      frontend/src/app/angular4-modules.ts
  2. 11
      frontend/src/app/components/input/auto-complete-helper.service.ts
  3. 2
      frontend/src/app/components/input/op-auto-complete.directive.ts
  4. 4
      frontend/src/app/components/wp-activity/user/user-activity.component.ts
  5. 42
      frontend/src/app/globals/augmenting/wiki-toolbar.augment.ts
  6. 91
      frontend/src/app/globals/augmenting/wiki-toolbar.ts
  7. 4
      frontend/src/app/modules/common/openproject-common.module.ts
  8. 56
      frontend/src/app/modules/common/wiki-toolbar/wiki-toolbar.directive.ts
  9. 7
      frontend/src/app/modules/fields/edit/editing-portal/edit-field-handler.interface.ts
  10. 6
      frontend/src/app/modules/fields/edit/field-types/formattable-edit-field.ts
  11. 11
      frontend/src/app/modules/fields/edit/field-types/formattable-textarea-edit-field.component.ts

@ -187,7 +187,6 @@ import {RevisionActivityComponent} from "core-components/wp-activity/revision/re
import {CommentService} from "core-components/wp-activity/comment-service";
import {WorkPackageCommentComponent} from "core-components/work-packages/work-package-comment/work-package-comment.component";
import {OpCkeditorFormComponent} from "core-components/ckeditor/op-ckeditor-form.component";
import {OpAutoCompleteDirective} from "core-components/input/op-auto-complete.directive";
import {WorkPackageUploadComponent} from "core-components/wp-attachments/wp-attachments-upload/wp-attachments-upload.component";
import {FocusWithinDirective} from "core-app/modules/common/focus/focus-within.directive";
import {OpDragScrollDirective} from "core-app/modules/common/ui/op-drag-scroll.directive";
@ -460,7 +459,6 @@ import {OpenProjectFileUploadService} from "core-components/api/op-file-upload/o
// CkEditor
OpCkeditorFormComponent,
OpAutoCompleteDirective,
],
entryComponents: [
WorkPackagesBaseComponent,

@ -51,10 +51,8 @@ export class AutoCompleteHelperService {
remoteFilter: (query:string, callback:Function) => {
const url:string = this.PathHelper.api.v3.principals(projectId, query);
this.http.get(url)
.subscribe((response:any) => {
if (response && response.data) {
const data = response.data;
.subscribe((data:any) => {
if (data) {
// atjs needs the search key to be a string
const principals = data["_embedded"]["elements"];
for (let i = principals.length - 1; i >= 0; i--) {
@ -95,9 +93,8 @@ export class AutoCompleteHelperService {
remoteFilter: (query:string, callback:Function) => {
if (query.length > 0) {
this.http.get(url, { params: { q: query, scope: 'all' } })
.subscribe(function(response:any) {
if (response && response.data) {
const data = response.data;
.subscribe(function(data:any) {
if (data) {
// atjs needs the search key to be a string
for (var i = data.length - 1; i >= 0; i--) {
data[i]['id_subject'] = data[i]['id'].toString() + ' ' + data[i]['subject'];

@ -31,7 +31,7 @@ import {Directive, ElementRef, Input, OnChanges} from "@angular/core";
import {AutoCompleteHelperService} from "core-components/input/auto-complete-helper.service";
@Directive({
selector: '.op-auto-complete, [op-auto-complete]',
selector: '[op-auto-complete]',
})
export class OpAutoCompleteDirective implements OnChanges {
@Input() public opAutoCompleteProjectId:string|null|undefined;

@ -209,6 +209,10 @@ export class UserActivityComponent implements IEditFieldHandler, OnInit, AfterVi
return `user_activity_edit_field_${this.activityNo}`;
}
public get project() {
return this.workPackage.project;
}
deactivate(focus:boolean):void {
this.inEdit = false;

@ -0,0 +1,42 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import {WikiToolbar} from "core-app/globals/augmenting/wiki-toolbar";
(function($:JQueryStatic) {
// Identify all uses
$(function () {
// Wrap all static wiki-toolbars (rendered from backend)
$('.wiki-toolbar')
.each((i:number, el:HTMLElement) => {
new WikiToolbar(I18n, el);
});
});
}(jQuery));

@ -0,0 +1,91 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
import {GlobalI18n} from "core-app/modules/common/i18n/i18n.service";
export class WikiToolbar {
public isPreview = false;
constructor(protected I18n:GlobalI18n,
protected element:HTMLElement,
public previewCallback?:(preview:boolean) => void) {
this.onInit();
}
onInit() {
const element = jQuery(this.element);
const help_link_title = this.I18n.t('js.inplace.link_formatting_help');
const button = document.createElement('button');
button.classList.add('jstb_help', 'formatting-help-link-button');
button.setAttribute('type', 'button');
button.setAttribute('aria-label', help_link_title);
button.setAttribute('title', help_link_title);
const PREVIEW_ENABLE_TEXT = this.I18n.t('js.inplace.btn_preview_enable');
const PREVIEW_DISABLE_TEXT = this.I18n.t('js.inplace.btn_preview_disable');
const PREVIEW_BUTTON_CLASS = 'jstb_preview';
let previewButtonAttributes:any = {
'class': PREVIEW_BUTTON_CLASS + ' icon-preview icon-small',
'type': 'button',
'title': PREVIEW_ENABLE_TEXT,
'aria-label': PREVIEW_ENABLE_TEXT,
'text': ''
};
let textarea = this.element;
if (!element.is('textarea')) {
textarea = this.element.querySelector('textarea') as any;
}
const wikiToolbar = new (window as any).jsToolBar(textarea);
wikiToolbar.setHelpLink(button);
wikiToolbar.draw();
previewButtonAttributes.click = () => {
this.isPreview = !this.isPreview;
!!this.previewCallback && this.previewCallback(this.isPreview);
const title = this.isPreview ? PREVIEW_DISABLE_TEXT : PREVIEW_ENABLE_TEXT;
const toggledClasses = 'icon-preview icon-ticket-edit -active';
element.closest('.textarea-wrapper')
.find('.' + PREVIEW_BUTTON_CLASS).attr('title', title)
.attr('aria-label', title)
.toggleClass(toggledClasses);
};
if (!!this.previewCallback) {
element
.closest('.textarea-wrapper')
.find('.jstb_help')
.after(jQuery('<button>', previewButtonAttributes));
}
}
}

@ -67,6 +67,7 @@ import {CopyToClipboardDirective} from "core-app/modules/common/copy-to-clipboar
import {WikiToolbarDirective} from "core-app/modules/common/wiki-toolbar/wiki-toolbar.directive";
import {RefreshOnFormChangesDirective} from "core-app/modules/common/remote/refresh-on-form-changes.directive";
import {RemoteFieldUpdaterDirective} from "core-app/modules/common/remote/remote-field-updater.directive";
import {OpAutoCompleteDirective} from "core-components/input/op-auto-complete.directive";
@NgModule({
imports: [
@ -91,6 +92,7 @@ import {RemoteFieldUpdaterDirective} from "core-app/modules/common/remote/remote
FocusDirective,
AuthoringComponent,
WorkPackageEditActionsBarComponent,
OpAutoCompleteDirective,
// Notifications
NotificationsContainerComponent,
@ -110,6 +112,7 @@ import {RemoteFieldUpdaterDirective} from "core-app/modules/common/remote/remote
CollapsibleSectionComponent,
RefreshOnFormChangesDirective,
RemoteFieldUpdaterDirective,
WikiToolbarDirective,
],
declarations: [
OpDatePickerComponent,
@ -128,6 +131,7 @@ import {RemoteFieldUpdaterDirective} from "core-app/modules/common/remote/remote
FocusDirective,
AuthoringComponent,
WorkPackageEditActionsBarComponent,
OpAutoCompleteDirective,
// Notifications
NotificationsContainerComponent,

@ -26,58 +26,34 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
import {Directive, ElementRef, OnInit} from "@angular/core";
import {AfterViewInit, Directive, ElementRef, EventEmitter, OnDestroy, Output} from "@angular/core";
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {WikiToolbar} from "core-app/globals/augmenting/wiki-toolbar";
@Directive({
selector: '[wiki-toolbar], .wiki-toolbar'
selector: '[op-wiki-toolbar]'
})
export class WikiToolbarDirective implements OnInit {
public isPreview:boolean = false;
export class WikiToolbarDirective implements AfterViewInit, OnDestroy {
@Output() onPreviewToggle = new EventEmitter<undefined>();
public instance:WikiToolbar;
constructor(readonly I18n:I18nService,
readonly elementRef:ElementRef) {
}
ngOnInit() {
const element = jQuery(this.elementRef.nativeElement);
const help_link_title = this.I18n.t('js.inplace.link_formatting_help');
const button = document.createElement('button');
button.classList.add('jstb_help formatting-help-link-button');
button.setAttribute('type', 'button');
button.setAttribute('aria-label', help_link_title);
button.setAttribute('title', help_link_title);
const PREVIEW_ENABLE_TEXT = this.I18n.t('js.inplace.btn_preview_enable');
const PREVIEW_DISABLE_TEXT = this.I18n.t('js.inplace.btn_preview_disable');
const PREVIEW_BUTTON_CLASS = 'jstb_preview';
let previewButtonAttributes:any = {
'class': PREVIEW_BUTTON_CLASS + ' icon-preview icon-small',
'type': 'button',
'title': PREVIEW_ENABLE_TEXT,
'aria-label': PREVIEW_ENABLE_TEXT,
'text': ''
};
const wikiToolbar = new (window as any).jsToolBar(this.elementRef.nativeElement);
wikiToolbar.setHelpLink(button);
wikiToolbar.draw();
ngAfterViewInit() {
this.instance = new WikiToolbar(
this.I18n,
this.elementRef.nativeElement,
() => {
this.onPreviewToggle.emit();
});
}
previewButtonAttributes.click = function() {
const title = this.isPreview ? PREVIEW_DISABLE_TEXT : PREVIEW_ENABLE_TEXT;
const toggledClasses = 'icon-preview icon-ticket-edit -active';
element.closest('.textarea-wrapper')
.find('.' + PREVIEW_BUTTON_CLASS).attr('title', title)
.attr('aria-label', title)
.toggleClass(toggledClasses);
};
element
.closest('.textarea-wrapper')
.find('.jstb_help')
.after(jQuery('<button>', previewButtonAttributes));
ngOnDestroy() {
// nothing to do
}
}

@ -26,6 +26,8 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {ProjectResource} from "core-app/modules/hal/resources/project-resource";
export interface IEditFieldHandler {
/**
* Whether the handler belongs to a larger edit mode form
@ -51,6 +53,11 @@ export interface IEditFieldHandler {
*/
errorMessageOnLabel?:string;
/**
* Project resource
*/
project:ProjectResource;
/**
* Stop event propagation
*/

@ -50,7 +50,7 @@ export class FormattableEditField extends EditField {
// Values used in template
public isBusy:boolean = false;
public isPreview:boolean = false;
public previewHtml:string;
public previewHtml:string = '';
public text = {
attachmentLabel: this.I18n.t('js.label_formattable_attachment_hint'),
save: this.I18n.t('js.inplace.button_save', { attribute: this.schema.name }),
@ -163,9 +163,9 @@ export class FormattableEditField extends EditField {
const link = form.previewMarkup.$link;
this.textileService.render(link, this.rawValue)
.then((result:any) => {
.then((result:string) => {
this.isBusy = false;
this.previewHtml = result.data;
this.previewHtml = result;
})
.catch(() => {
this.isBusy = false;

@ -32,12 +32,17 @@ import {ExpressionService} from "../../../../../../common/expression.service";
@Component({
template: `
<div class="textarea-wrapper" ng-class="{'-preview': vm.field.isPreview}">
<div class="textarea-wrapper"
op-wiki-toolbar
(onPreviewToggle)="field.togglePreview()"
[ngClass]="{'-preview': field.isPreview}">
<textarea
style="min-height: 114px"
op-auto-complete
[opAutoCompleteProjectId]="handler.project.idFromLink"
class="focus-input wp-inline-edit--field inplace-edit--textarea -animated"
name="value"
*ngIf="!field.isPreview"
[hidden]="field.isPreview"
[disabled]="field.isBusy || field.inFlight"
[required]="field.required"
[(ngModel)]="field.rawValue"
@ -66,6 +71,6 @@ export class FormattableTextareaEditFieldComponent extends EditFieldComponent {
public field:FormattableEditField;
public get unEscapedPreviewHtml() {
return ExpressionService.unescape(this.field.previewHtml);
return ExpressionService.unescape(this.field.previewHtml || '');
}
}

Loading…
Cancel
Save