From 46cf6c823ca8ab628cd8c5bd7fdfe8945344a014 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 16 May 2023 14:50:29 -0400 Subject: fixed bugs with goldenlayout dragging and undoing. fixed searching for filter values in sidebars. Stopped creating empty list for collections when datafield() is accessed because it messes up undo of a collection. fixed tab title editing. from marquee. Added UndoStack UI and additional naming support in code. --- src/client/views/nodes/AudioBox.tsx | 2 +- src/client/views/nodes/DocumentLinksButton.tsx | 20 +++---- src/client/views/nodes/DocumentView.tsx | 57 ++++++++++-------- src/client/views/nodes/LinkDocPreview.tsx | 3 +- src/client/views/nodes/button/FontIconBox.tsx | 60 ++++++++----------- .../views/nodes/formattedText/FormattedTextBox.tsx | 69 +++++++++++----------- src/client/views/nodes/trails/PresBox.tsx | 9 ++- src/client/views/nodes/trails/PresElementBox.tsx | 13 ++-- 8 files changed, 113 insertions(+), 120 deletions(-) (limited to 'src/client/views/nodes') diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 8e83cf121..410e0bbdc 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); - setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = time); + setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); playing = () => this.mediaState === media_state.Playing; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index d5ca30957..bd1952ecb 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; @@ -78,7 +78,6 @@ export class DocumentLinksButton extends React.Component { setupMoveUpEvents( this, @@ -123,17 +122,14 @@ export class DocumentLinksButton extends React.Component { - DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View); - }) - ) + action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)) ); }; - public static finishLinkClick = undoBatch( - action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => { - if (startLink === endLink) { + @undoBatch + public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) { + runInAction(() => { + if (startLink === endLink || !startLink) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; @@ -185,8 +181,8 @@ export class DocumentLinksButton extends React.Component (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); + clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title)); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { @@ -490,7 +491,7 @@ export class DocumentViewInternal extends DocComponent { - this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000); + this._longPressSelector = setTimeout(() => { + if (DocumentView.LongPress) { + if (this.rootDoc.dontUndo) { + OverlayView.Instance.addWindow(, { x: 300, y: 100, width: 200, height: 200, title: 'Undo Stack' }); + } else { + this.props.select(false); + } + } + }, 1000); if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); this._downX = e.clientX; @@ -557,7 +566,7 @@ export class DocumentViewInternal extends DocComponent { this._markerTargetDoc = linkTarget; this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } - this._toolTipText = 'link to ' + this._targetDoc?.title; if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) @@ -296,7 +295,7 @@ export class LinkDocPreview extends React.Component { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> {this.docPreview} ); diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 57aa852ac..b07cf7e00 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -16,7 +16,7 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp import { LinkManager } from '../../../util/LinkManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; @@ -129,29 +129,27 @@ export class FontIconBox extends DocComponent() { * Number button */ @computed get numberSliderButton() { - const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); - + const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined }); // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); - + const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); const label = !FontIconBox.GetShowLabels() ? null :
{this.label}
; const dropdown = (
e.stopPropagation()}> (this._batch = UndoManager.StartBatch('presDuration'))} + onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))} onPointerUp={() => this._batch?.end()} - onChange={e => { + onChange={undoable(e => { e.stopPropagation(); - setValue(Number(e.target.value)); - }} + numScript(Number(e.target.value)); + }, 'set num value')} />
); @@ -174,20 +172,13 @@ export class FontIconBox extends DocComponent() { * Number button */ @computed get numberDropdownButton() { - const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); - - // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); + const numScript = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined }); - const label = !FontIconBox.GetShowLabels() ? null :
{this.label}
; + const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); const items: number[] = []; - for (let i = 0; i < 100; i++) { - if (i % 2 === 0) { - items.push(i); - } - } + for (let i = 0; i < 100; i += 2) items.push(i); + const list = items.map(value => { return (
() { style={{ backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined, }} - onClick={() => setValue(value)}> + onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}> {value}
); }); return (
-
setValue(Number(checkResult) - 1))}> - +
numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}> +
() { this.noTooltip = this.rootDoc.dropDownOpen; Doc.UnBrushAllDocs(); })}> - setValue(Number(e.target.value))))} /> + numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} />
-
setValue(Number(checkResult) + 1))}> +
numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}>
@@ -322,12 +313,12 @@ export class FontIconBox extends DocComponent() { .map(value => (
script.script.run({ self: this.rootDoc, value }))}> + onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}> {value[0].toUpperCase() + value.slice(1)}
)); @@ -640,29 +631,26 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); // prettier-ignore - const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ + const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ ['grid', { - undo: false, checkResult: (doc:Doc) => doc._freeform_backgroundGrid, setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, }], ['snaplines', { - undo: false, checkResult: (doc:Doc) => doc._freeform_snapLines, setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines, }], ['viewAll', { - undo: false, checkResult: (doc:Doc) => doc._freeform_fitContentsToBox, setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, }], ['clusters', { - undo: false, + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => doc._freeform_useClusters, setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters, }], ['arrange', { - undo: true, + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => doc._autoArrange, setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, }], @@ -671,7 +659,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'cluster if (checkResult) { return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent'; } - const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} }; + const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} }; SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); setTimeout(() => batch.end(), 100); }); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9c06aa7d8..e85835002 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -301,11 +301,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); @@ -320,29 +320,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent(Array.from(new Set(accumTags))) : undefined; let unchanged = true; - if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { + if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) { this._applyingChange = this.fieldKey; - const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text; + const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); - if ((!curTemp && !curProto) || curText || json.includes('dash')) { + if ((!prevData && !protoData) || newText || (!newText && !protoData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (removeSelection(json) !== removeSelection(curLayout?.Data)) { + if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); - if (numstring !== undefined) { - dataDoc[this.fieldKey] = Number(curText); - } else { - dataDoc[this.fieldKey] = new RichTextField(json, curText); - } + dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited - textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } } else { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have - ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } this._applyingChange = ''; @@ -667,7 +663,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const batch = UndoManager.StartBatch('sidebar'); + const batch = UndoManager.StartBatch('toggle sidebar'); setupMoveUpEvents( this, e, @@ -694,13 +690,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { + const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); // this.props.dataDoc[this.fieldKey] = new List(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); this.props.select(false); + setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch @@ -733,17 +730,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent h); const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0]; + const deleteMarkups = undoBatch(() => { + const sel = editor.state.selection; + editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + }); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( action(anchor => { + anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc); AnchorMenu.Instance.Status = 'annotation'; - AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc); + AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc); AnchorMenu.Instance.Pinned = false; - AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc); - AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc); - AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc); - AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc); + AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc); + AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc); + AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc); + AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); }) ); @@ -1002,7 +1004,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); this._editorView.state.storedMarks = [ ...(this._editorView.state.storedMarks ?? []), @@ -1630,7 +1631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { //applyDevTools.applyDevTools(this._editorView); this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); - this.startUndoTypingBatch(); + e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { @@ -1705,13 +1706,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent() { min={min} max={max} value={value} + readOnly={true} style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { @@ -1501,7 +1502,7 @@ export class PresBox extends ViewBoxBaseComponent() {
Slide Duration
- e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
this.updateDurationTime(String(duration), 1000)}> @@ -1654,7 +1655,7 @@ export class PresBox extends ViewBoxBaseComponent() {
Zoom (% screen filled)
- this.updateZoom(e.target.value)} />% + this.updateZoom(e.target.value)} />%
this.updateZoom(String(zoom), 0.1)}> @@ -1669,7 +1670,7 @@ export class PresBox extends ViewBoxBaseComponent() {
Transition Time
- e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
this.updateTransitionTime(String(transitionSpeed), 1000)}> @@ -1756,6 +1757,7 @@ export class PresBox extends ViewBoxBaseComponent() { className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presStartTime).toFixed(2)} onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent) => { @@ -1782,6 +1784,7 @@ export class PresBox extends ViewBoxBaseComponent() { onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presEndTime).toFixed(2)} onChange={action((e: React.ChangeEvent) => { activeItem.presEndTime = Number(e.target.value); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 4eb6aee25..2279ffe84 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; @@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; @@ -263,16 +263,15 @@ export class PresElementBox extends ViewBoxBaseComponent() { } }; - @undoBatch - removeItem = action((e: React.MouseEvent) => { + removePresentationItem = undoable((e: React.MouseEvent) => { e.stopPropagation(); if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { - this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; + runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1)); } this.props.removeDocument?.(this.rootDoc); this.presBoxView?.removeFromSelectedArray(this.rootDoc); this.removeAllRecordingInOverlay(); - }); + }, 'Remove doc from pres trail'); // set the value/title of the individual pres element @undoBatch @@ -476,7 +475,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { ); items.push( Remove from presentation
}> -
+
e.stopPropagation()} />
-- cgit v1.2.3-70-g09d2