From 0c387604f84aeb1b073b3bad842a7a0ee1615d66 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 13 Dec 2024 16:23:56 -0700 Subject: [PATCH] Change diff widget to webcomponent, update diff library --- Gruntfile.js | 8 +- api/js/etemplate/Et2Diff/Et2Diff.md | 45 +++++ api/js/etemplate/Et2Diff/Et2Diff.ts | 172 +++++++++++++++++++ api/js/etemplate/et2_widget_diff.ts | 193 +--------------------- api/js/etemplate/et2_widget_historylog.ts | 19 ++- api/js/etemplate/etemplate2.ts | 1 + api/templates/default/etemplate2.css | 54 ++---- composer.json | 1 - doc/etemplate2/_includes/default.njk | 1 + doc/etemplate2/eleventy.config.cjs | 2 +- package-lock.json | 73 ++++++++ package.json | 1 + 12 files changed, 329 insertions(+), 241 deletions(-) create mode 100644 api/js/etemplate/Et2Diff/Et2Diff.md create mode 100644 api/js/etemplate/Et2Diff/Et2Diff.ts diff --git a/Gruntfile.js b/Gruntfile.js index 397bc98b19..d0c2b51a0f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -41,7 +41,7 @@ module.exports = function (grunt) { files: { "pixelegg/css/fancy.min.css": [ "node_modules/flatpickr/dist/themes/light.css", - "vendor/bower-asset/diff2html/dist/diff2html.css", + "node_modules/diff2html/bundles/css/diff2html.min.css", "vendor/bower-asset/cropper/dist/cropper.min.css", "api/templates/default/css/flags.css", "api/templates/default/css/htmlarea.css", @@ -52,7 +52,7 @@ module.exports = function (grunt) { ], "pixelegg/css/pixelegg.min.css": [ "node_modules/flatpickr/dist/themes/light.css", - "vendor/bower-asset/diff2html/dist/diff2html.css", + "node_modules/diff2html/bundles/css/diff2html.min.css", "vendor/bower-asset/cropper/dist/cropper.min.css", "api/templates/default/css/flags.css", "api/templates/default/css/htmlarea.css", @@ -63,7 +63,7 @@ module.exports = function (grunt) { ], "pixelegg/css/mobile.min.css": [ "node_modules/flatpickr/dist/themes/light.css", - "vendor/bower-asset/diff2html/dist/diff2html.css", + "node_modules/diff2html/bundles/css/diff2html.min.css", "vendor/bower-asset/cropper/dist/cropper.min.css", "api/templates/default/css/flags.css", "api/templates/default/css/htmlarea.css", @@ -74,7 +74,7 @@ module.exports = function (grunt) { ], "pixelegg/mobile/fw_mobile.min.css": [ "node_modules/flatpickr/dist/themes/light.css", - "api/js/etemplate/lib/jsdifflib/diffview.css", + "node_modules/diff2html/bundles/css/diff2html.min.css", "vendor/bower-asset/cropper/dist/cropper.min.css", "api/templates/default/css/flags.css", "api/templates/default/css/htmlarea.css", diff --git a/api/js/etemplate/Et2Diff/Et2Diff.md b/api/js/etemplate/Et2Diff/Et2Diff.md new file mode 100644 index 0000000000..4c83fd9cb0 --- /dev/null +++ b/api/js/etemplate/Et2Diff/Et2Diff.md @@ -0,0 +1,45 @@ +```html:preview + + +``` + +Shows a snippet of a [diff](https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html), and if you +click on it shows a dialog with the whole diff. + +## Examples + +### noDialog + +Use the noDialog property to disable the size limit and show the whole diff + +```html:preview + + +``` \ No newline at end of file diff --git a/api/js/etemplate/Et2Diff/Et2Diff.ts b/api/js/etemplate/Et2Diff/Et2Diff.ts new file mode 100644 index 0000000000..727707c6e3 --- /dev/null +++ b/api/js/etemplate/Et2Diff/Et2Diff.ts @@ -0,0 +1,172 @@ +import {customElement} from "lit/decorators/custom-element.js"; +import {css, html, LitElement, nothing, PropertyValueMap, render} from "lit"; +import {classMap} from "lit/directives/class-map.js"; +import {unsafeHTML} from "lit/directives/unsafe-html.js"; +import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget"; +import * as Diff2Html from "diff2html"; +import {Diff2HtmlConfig} from "diff2html"; +import {ColorSchemeType} from "diff2html/lib/types"; +import {property} from "lit/decorators/property.js"; +import shoelace from "../Styles/shoelace"; +import {Et2Dialog} from "../Et2Dialog/Et2Dialog"; + +/** + * Show a nicely formatted diff + */ +@customElement("et2-diff") +export class Et2Diff extends Et2InputWidget(LitElement) +{ + @property({type: Boolean, reflect: true}) + open = false; + + /** + * Disable the dialog and show the whole diff + * + * @type {boolean} + */ + @property({type: Boolean, reflect: true}) + noDialog = false; + + // CSS in etemplate2.css due to library + static get styles() + { + return [ + shoelace, + ...super.styles, + css` + :host { + position: relative; + } + + .expand-icon { + display: none; + position: absolute; + bottom: var(--sl-spacing-medium); + right: var(--sl-spacing-medium); + background-color: var(--sl-panel-background-color); + z-index: 1; + } + + :host(:hover) { + .expand-icon { + display: initial; + } + } + + :host(:not([open])) { + cursor: pointer; + } + + :host(:not([noDialog])) .form-control-input { + max-height: 9em; + overflow: hidden; + } + ` + ]; + } + + + private readonly diff_options : Diff2HtmlConfig = { + matching: "words", + drawFileList: false, + colorScheme: ColorSchemeType.AUTO + }; + + updated(changedProperties : PropertyValueMap) + { + if(changedProperties.has("value") || this.value && this.childElementCount == 0) + { + // Put diff into lightDOM so styles can leak, since we can't import the library CSS into the component + render(html`${unsafeHTML(Diff2Html.html(this.value ?? "", this.diff_options))}`, this, {host: this}); + } + } + + set value(value : string) + { + if(typeof value == 'string') + { + + // Diff2Html likes to have files, we don't have them + if(value.indexOf('---') !== 0) + { + value = "--- diff\n+++ diff\n" + value; + } + + super.value = value; + this.requestUpdate("value"); + } + } + + _handleClick(e) + { + const oldValue = this.getAttribute("open") + this.toggleAttribute("open"); + this.requestUpdate("open", oldValue); + } + + getDetachedAttributes(attrs) + { + attrs.push("id", "value", "class"); + } + + getDetachedNodes() : HTMLElement[] + { + return [this]; + } + + setDetachedAttributes(_nodes : HTMLElement[], _values : object, _data? : any) : void + { + for(let attr in _values) + { + this[attr] = _values[attr]; + } + } + + render() + { + const labelTemplate = this._labelTemplate(); + const helpTextTemplate = this._helpTextTemplate(); + return html` +
+ ${labelTemplate} +
+ + ${this.open && !this.noDialog ? + html` + + { + // Stop bubble or it will re-show dialog + e.stopPropagation() + }} + @close=${() => + { + this.removeAttribute("open"); + this.requestUpdate("open", true); + }} + > + + ` : html` + ${!this.noDialog ? html` + ` : nothing} + ` + } +
+ ${helpTextTemplate} +
+ `; + } +} \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_diff.ts b/api/js/etemplate/et2_widget_diff.ts index fefb03ea29..b3a74be065 100644 --- a/api/js/etemplate/et2_widget_diff.ts +++ b/api/js/etemplate/et2_widget_diff.ts @@ -9,202 +9,15 @@ * @copyright Nathan Gray 2012 */ -/*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - /vendor/bower-asset/diff2html/dist/diff2html.min.js; - et2_core_valueWidget; -*/ -import "../../../vendor/bower-asset/diff2html/dist/diff2html.min"; -import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; -import {ClassWithAttributes} from "./et2_core_inheritance"; -import {et2_valueWidget} from "./et2_core_valueWidget"; -import {et2_IDetachedDOM} from "./et2_core_interfaces"; -import {Et2Dialog} from "./Et2Dialog/Et2Dialog"; +import {et2_register_widget} from "./et2_core_widget"; +import {Et2Diff} from "./Et2Diff/Et2Diff"; /** * Class that displays the diff between two [text] values * * @augments et2_valueWidget */ -export class et2_diff extends et2_valueWidget implements et2_IDetachedDOM +export class et2_diff extends Et2Diff { - static readonly _attributes = { - "value": { - "type": "any" - } - }; - - private readonly diff_options: { - "inputFormat":"diff", - "matching": "words" - }; - private div: HTMLDivElement; - private mini: boolean = true; - - /** - * Constructor - */ - constructor(_parent, _attrs? : WidgetConfig, _child? : object) - { - // Call the inherited constructor - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_diff._attributes, _child || {})); - - // included via etemplate2.css - //this.egw().includeCSS('../../../vendor/bower-asset/dist/dist2html.css'); - this.div = document.createElement("div"); - jQuery(this.div).addClass('et2_diff'); - } - - set_value( value) - { - jQuery(this.div).empty(); - if(typeof value == 'string') { - - // Diff2Html likes to have files, we don't have them - if(value.indexOf('---') !== 0) - { - value = "--- diff\n+++ diff\n"+value; - } - // @ts-ignore - var diff = Diff2Html.getPrettyHtml(value, this.diff_options); - // var ui = new Diff2HtmlUI({diff: diff}); - // ui.draw(jQuery(this.div), this.diff_options); - jQuery(this.div).append(diff); - } - else if(typeof value != 'object') - { - jQuery(this.div).append(value); - } - this.check_mini(); - } - - check_mini( ) - { - if(!this.mini) - { - return false; - } - var view = jQuery(this.div).children(); - this.minify(view); - var self = this; - jQuery(' ') - .appendTo(self.div) - .css("cursor", "pointer") - .click({diff: view, div: self.div, label: self.options.label}, function(e) - { - var diff = e.data.diff; - var div = e.data.div; - self.un_minify(diff); - let dialog = new Et2Dialog(self.egw()); - - dialog.transformAttributes({ - title: e.data.label, - //modal: true, - buttons: [{label: 'ok'}], - class: "et2_diff", - }); - diff.attr("slot", "content"); - dialog.addEventListener("open", () => - { - diff.appendTo(dialog); - if(jQuery(this).parent().height() > jQuery(window).height()) - { - jQuery(this).height(jQuery(window).height() * 0.7); - } - }); - dialog.addEventListener("close", () => - { - // Need to destroy the dialog, etemplate widget needs divs back where they were - self.minify(this); - - // Put it back where it came from, or et2 will error when clear() is called - diff.prependTo(div); - }); - document.body.appendChild(dialog); - }); - } - set_label( _label) - { - this.options.label = _label; - - } - - /** - * Make the diff into a mini-diff - * - * @param {DOMNode|String} view - */ - minify( view) - { - view = jQuery(view) - .addClass('mini') - // Dialog changes these, if resized - .css('height', 'inherit') - .show(); - jQuery('th', view).hide(); - jQuery('td.equal',view).hide() - .prevAll().hide(); - } - - /** - * Expand mini-diff - * - * @param {DOMNode|String} view - */ - un_minify( view) - { - jQuery(view).removeClass('mini').show(); - jQuery('th',view).show(); - jQuery('td.equal',view).show(); - } - - /** - * Code for implementing et2_IDetachedDOM - * Fast-clonable read-only widget that only deals with DOM nodes, not the widget tree - */ - - /** - * Build a list of attributes which can be set when working in the - * "detached" mode in the _attrs array which is provided - * by the calling code. - * - * @param {object} _attrs - */ - getDetachedAttributes(_attrs) - { - _attrs.push("value", "label"); - } - - /** - * Returns an array of DOM nodes. The (relativly) same DOM-Nodes have to be - * passed to the "setDetachedAttributes" function in the same order. - */ - getDetachedNodes() - { - return [this.div]; - } - - /** - * Sets the given associative attribute->value array and applies the - * attributes to the given DOM-Node. - * - * @param _nodes is an array of nodes which has to be in the same order as - * the nodes returned by "getDetachedNodes" - * @param _values is an associative array which contains a subset of attributes - * returned by the "getDetachedAttributes" function and sets them to the - * given values. - */ - setDetachedAttributes(_nodes, _values) - { - this.div = _nodes[0]; - if(typeof _values['label'] != 'undefined') - { - this.set_label(_values['label']); - } - if(typeof _values['value'] != 'undefined') - { - this.set_value(_values['value']); - } - } } et2_register_widget(et2_diff, ["diff"]); \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_historylog.ts b/api/js/etemplate/et2_widget_historylog.ts index ba1723df54..443ba090e0 100644 --- a/api/js/etemplate/et2_widget_historylog.ts +++ b/api/js/etemplate/et2_widget_historylog.ts @@ -25,12 +25,12 @@ import {et2_valueWidget} from "./et2_core_valueWidget"; import {et2_dataview} from "./et2_dataview"; import {et2_dataview_column} from "./et2_dataview_model_columns"; import {et2_dataview_controller} from "./et2_dataview_controller"; -import {et2_diff} from "./et2_widget_diff"; import {et2_IDetachedDOM, et2_IResizeable} from "./et2_core_interfaces"; import {et2_customfields_list} from "./et2_extension_customfields"; import {et2_selectbox} from "./et2_widget_selectbox"; import {loadWebComponent} from "./Et2Widget/Et2Widget"; import {cleanSelectOptions, SelectOption} from "./Et2Select/FindSelectOptions"; +import {Et2Diff} from "./Et2Diff/Et2Diff"; /** * eTemplate history log widget displays a list of changes to the current record. @@ -98,7 +98,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider private dataview: et2_dataview; private controller: et2_dataview_controller; private fields: any; - private diff: et2_diff; + private diff : { nodes : Et2Diff, widget : Et2Diff }; /** * Constructor @@ -399,11 +399,10 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider }; } // Widget for text diffs - const diff = et2_createWidget('diff', {}, this); + const diff = loadWebComponent('et2-diff', {}, this); this.diff = { - // @ts-ignore widget: diff, - nodes: jQuery(diff.getDetachedNodes()) + nodes: diff }; } @@ -656,7 +655,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider widget = self.diff.widget; nodes = self.diff.nodes.clone(); - if(widget) widget.setDetachedAttributes(nodes, { + if(nodes) nodes.setDetachedAttributes(nodes, { value: value, label: jthis.parents("td").prev().text() }); @@ -678,7 +677,11 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider { const id = widget._children[j].id; const widget_value = value ? value[id] || "" : ""; - widget._children[j].setDetachedAttributes(nodes[j], {value: widget_value}); + widget._children[j].setDetachedAttributes(widget._children[j], {value: widget_value}); + if(widget._children[j] instanceof Et2Diff) + { + self._spanValueColumns(jQuery(this)); + } } } else @@ -754,7 +757,7 @@ export class et2_historylog extends et2_valueWidget implements et2_IDataProvider if(this.dataview) { const columns = this.dataview.getColumnMgr(); - jQuery('.et2_diff', this.div).closest('.innerContainer') + jQuery('[colspan=2]', this.div).find('.innerContainer') .width(columns.getColumnWidth(et2_historylog.NEW_VALUE) + columns.getColumnWidth(et2_historylog.OLD_VALUE)); } } diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 916d8f8f76..f19feddb00 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -54,6 +54,7 @@ import './Et2Date/Et2DateTimeToday'; import './Et2Description/Et2Description'; import './Et2Dialog/Et2Dialog'; import './Et2Dialog/Et2MergeDialog'; +import './Et2Diff/Et2Diff'; import './Et2DropdownButton/Et2DropdownButton'; import './Et2Email/Et2Email'; import './Expose/Et2ImageExpose'; diff --git a/api/templates/default/etemplate2.css b/api/templates/default/etemplate2.css index e7fdc7ebb0..9c2c9dbd81 100644 --- a/api/templates/default/etemplate2.css +++ b/api/templates/default/etemplate2.css @@ -883,62 +883,42 @@ for printing /** * Diff widget */ -div.et2_diff { - width: 100%; -} -.et2_diff thead, -.author, -.d2h-file-header, -.d2h-file-info, -.d2h-info, -.et2_diff:not(.ui-dialog-content) .d2h-cntx { + +et2-diff thead, +et2-diff .author, +et2-diff .d2h-file-header, +et2-diff .d2h-file-info, +et2-diff .d2h-info { display: none; } -.d2h-file-diff { +et2-diff .d2h-file-diff { white-space: normal; } -.et2_diff .d2h-file-wrapper { -} - -.et2_diff .d2h-file-diff { +et2-diff .d2h-file-diff { overflow-x: hidden; } -.et2_diff .d2h-code-wrapper { +et2-diff .d2h-code-wrapper { position: relative; } -.ui-widget-content .d2h-code-line-ctn { - white-space: normal; +et2-diff .d2h-del, et2-diff.d2h-del.d2h-change, et2-diff .d2h-file-diff .d2h-del.d2h-change { + background-color: var(--sl-color-red-100, #ffeef0); } -.ui-widget-content .d2h-file-diff { - overflow-x: visible; - overflow-y: visible; +et2-diff .d2h-code-line del, et2-diff .d2h-code-side-line del { + background-color: var(--sl-color-red-300, #fdb8c0); } -.et2_diff .ui-icon { - margin-top: -14px; - float: right; +et2-diff .d2h-ins, et2-diff.d2h-ins.d2h-change, et2-diff .d2h-file-diff .d2h-ins.d2h-change { + background-color: var(--sl-color-green-100, #e6ffed); } -.et2_diff .d2h-del, .et2_diff.d2h-del.d2h-change, .et2_diff .d2h-file-diff .d2h-del.d2h-change { - background-color: #ffeef0; -} - -.et2_diff .d2h-code-line del, .et2_diff .d2h-code-side-line del { - background-color: #fdb8c0; -} - -.et2_diff .d2h-ins, .et2_diff.d2h-ins.d2h-change, .et2_diff .d2h-file-diff .d2h-ins.d2h-change { - background-color: #e6ffed; -} - -.et2_diff .d2h-code-line ins, .et2_diff .d2h-code-side-line ins { - background-color: #acf2bd; +et2-diff .d2h-code-line ins, et2-diff .d2h-code-side-line ins { + background-color: var(--sl-color-green-300, #acf2bd); } /** Display a loading icon **/ diff --git a/composer.json b/composer.json index c5ba43cc93..d85ef71833 100644 --- a/composer.json +++ b/composer.json @@ -75,7 +75,6 @@ "async-aws/s3": "^2.0", "bigbluebutton/bigbluebutton-api-php": "^2.0", "bower-asset/cropper": "2.3.*", - "bower-asset/diff2html": "^2.7", "bower-asset/jquery": "^1.12.4", "defuse/php-encryption": "^2.4", "egroupware/activesync": "self.version", diff --git a/doc/etemplate2/_includes/default.njk b/doc/etemplate2/_includes/default.njk index bd42401611..c90c277668 100644 --- a/doc/etemplate2/_includes/default.njk +++ b/doc/etemplate2/_includes/default.njk @@ -43,6 +43,7 @@ + {# Set the initial theme and menu states here to prevent flashing #}