aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/LabelBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/LabelBox.tsx')
-rw-r--r--src/client/views/nodes/LabelBox.tsx171
1 files changed, 128 insertions, 43 deletions
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 8974cccaf..7fb83571f 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -1,19 +1,23 @@
import { Property } from 'csstype';
-import { action, computed, makeObservable } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as textfit from 'textfit';
-import { Field, FieldType } from '../../../fields/Doc';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { Doc, Field } from '../../../fields/Doc';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
+import { undoable } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { FieldView, FieldViewProps } from './FieldView';
import './LabelBox.scss';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { RichTextMenu } from './formattedText/RichTextMenu';
+import { DocumentView } from './DocumentView';
@observer
export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -22,7 +26,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: NodeJS.Timeout | undefined;
- _divRef: HTMLDivElement | null = null;
+ private _divRef: HTMLDivElement | null = null;
+ private _reaction: IReactionDisposer | undefined;
constructor(props: FieldViewProps) {
super(props);
@@ -36,22 +41,28 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @computed get Title() {
- return Field.toString(this.dataDoc[this.fieldKey] as FieldType) || StrCast(this.Document.title);
- }
-
- @computed get backgroundColor() {
- return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string;
- }
-
componentDidMount() {
this._props.setContentViewBox?.(this);
+ this._reaction = reaction(
+ () => this.Title,
+ () => document.activeElement !== this._divRef && this._forceRerender++
+ );
}
componentWillUnMount() {
this._timeout && clearTimeout(this._timeout);
+ this.setText(this._divRef?.innerText ?? '');
+ this._reaction?.();
}
- specificContextMenu = (): void => {};
+ @observable _forceRerender = 0;
+
+ @computed get Title() { return Field.toString(this.dataDoc[this.fieldKey]); } // prettier-ignore
+ @computed get backgroundColor() { return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } // prettier-ignore
+ @computed get boxShadow() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string; } // prettier-ignore
+
+ setText = undoable((text: string) => {
+ this.dataDoc[this.fieldKey] = text;
+ }, 'set label text');
drop = (/* e: Event, de: DragManager.DropEvent */) => {
return false;
@@ -84,10 +95,11 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
const textfitParams = {
minFontSize: NumCast(this.layoutDoc._label_minFontSize, 1),
maxFontSize: NumCast(this.layoutDoc._label_maxFontSize, 100),
- multiLine: BoolCast(this.layoutDoc._singleLine, true) ? false : true,
- alignHoriz: true,
+ multiLine: r?.textContent?.includes('\n') ? true : false,
+ // hack because tetFit doesn't support align 'right', but we need mobx to invalidate, so treat null as false and set to right inline
+ alignHoriz: StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'center' ? true : StrCast(this.layoutDoc[this.fieldKey + '_align']) === 'right' ? (null as unknown as boolean) : false,
alignVert: true,
- detectMultiLine: true,
+ detectMultiLine: false,
};
if (r) {
if (!r.offsetHeight || !r.offsetWidth) {
@@ -96,67 +108,140 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._timeout = setTimeout(() => this.fitTextToBox(r));
return textfitParams;
}
+ r.style.whiteSpace = ''; // textfit sets to nowrap if not multiline, but doesn't reeset if it becomes multiline
+ r.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']); // textfit doesn't reset textAlign if it has been set to center, so we just set it to what we want
+ r.firstChild instanceof HTMLElement && (r.firstChild.style.textAlign = StrCast(this.layoutDoc[this.fieldKey + '_align']));
textfit(r, textfitParams);
}
return textfitParams;
};
+ resetCursor = (cranchor?: number) => {
+ if (this._divRef && (cranchor || this._divRef === document.activeElement)) {
+ const range = document.createRange();
+ const anchor = cranchor ?? this._divRef.childNodes.length;
+ const container = cranchor === undefined ? this._divRef : (this._divRef.firstChild?.firstChild ?? this._divRef);
+ range.setStart(container, anchor);
+ range.setEnd(container, anchor);
+ const sel = window.getSelection();
+ sel?.removeAllRanges();
+ sel?.addRange(range);
+ }
+ };
+
+ beforeInput = action((event: InputEvent) => {
+ const spanChild = this._divRef?.firstChild?.firstChild;
+ if (spanChild?.nodeName === '#text' && ['insertLineBreak', 'insertParagraph'].includes(event.inputType)) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const selection = document.getSelection();
+ if (selection && document.activeElement === event.target) {
+ const text = spanChild.textContent ?? '';
+ const cranchor = selection.anchorNode === this._divRef ? (selection.anchorOffset ? text.length : 0) : selection.anchorOffset;
+ const addReturnHack = text.length <= cranchor && text[text.length - 1] !== '\n' ? '\n\n' : '\n'; // not sure why, but need to add a second carriage return if typing enter at the end of the text
+ const splitText = text.substring(0, cranchor) + addReturnHack + text.substring(cranchor);
+ spanChild.textContent = splitText;
+ this.resetCursor(cranchor + addReturnHack.length);
+ }
+ // const span = document.createElement('span');
+ // span.innerHTML = '&#8203;';
+ // this._divRef!.append(span);
+ }
+ });
+ // .labelBox-mainButton > div > span:nth-child(2) {
+
+ /**
+ * When an IconButton is clicked, it will receive focus. However, we don't want that since we want or need that since we really want
+ * to maintain focus in the label's editing div (and cursor position). so this relies on IconButton's having a tabindex set to -1 so that
+ * we can march up the tree from the 'relatedTarget' to determine if the loss of focus was caused by a fonticonbox. If it is, we then
+ * restore focus
+ * @param e focusout event on the editing div
+ */
+ keepFocus = (e: FocusEvent) => {
+ if (e.relatedTarget instanceof HTMLElement && e.relatedTarget.tabIndex === -1) {
+ for (let ele: HTMLElement | null = e.relatedTarget; ele; ele = (ele as HTMLElement)?.parentElement) {
+ if ((ele as HTMLElement)?.className === 'fonticonbox') {
+ setTimeout(() => this._divRef?.focus());
+ break;
+ }
+ }
+ }
+ };
+
render() {
TraceMobx();
const boxParams = this.fitTextToBox(undefined); // this causes mobx to trigger re-render when data changes
- const label = this.Title.startsWith('#') ? null : this.Title;
return (
- <div key={label?.length} className="labelBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu} style={{ boxShadow: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BoxShadow) as string }}>
+ <div className="labelBox-outerDiv" ref={this.createDropTarget} style={{ boxShadow: this.boxShadow }}>
<div
className="labelBox-mainButton"
style={{
backgroundColor: this.backgroundColor,
- // fontSize: StrCast(this.layoutDoc._text_fontSize),
- color: StrCast(this.layoutDoc._color),
- fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
+ color: StrCast(this.layoutDoc._text_fontColor, StrCast(this.layoutDoc._color)),
+ fontFamily: StrCast(this.layoutDoc._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
- textTransform: StrCast(this.layoutDoc.textTransform) as Property.TextTransform,
+ textTransform: StrCast(this.layoutDoc[this.fieldKey + '_transform']) as Property.TextTransform,
paddingLeft: NumCast(this.layoutDoc._xPadding),
paddingRight: NumCast(this.layoutDoc._xPadding),
paddingTop: NumCast(this.layoutDoc._yPadding),
paddingBottom: NumCast(this.layoutDoc._yPadding),
width: this._props.PanelWidth(),
height: this._props.PanelHeight(),
- whiteSpace: 'multiLine' in boxParams && boxParams.multiLine ? 'pre-wrap' : 'pre',
+ whiteSpace: boxParams.multiLine ? 'pre-wrap' : 'pre',
}}>
<div
+ key={this._forceRerender}
style={{
width: this._props.PanelWidth() - 2 * NumCast(this.layoutDoc._xPadding),
height: this._props.PanelHeight() - 2 * NumCast(this.layoutDoc._yPadding),
outline: 'unset !important',
}}
- onKeyDown={action(e => {
+ onKeyDown={e => {
e.stopPropagation();
- })}
+ }}
onKeyUp={action(e => {
e.stopPropagation();
- if (e.key === 'Enter') {
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
- setTimeout(() => this._props.select(false));
+ const text = this._divRef?.firstChild;
+ if (text && (text as HTMLElement)?.nodeType === 3) {
+ this._divRef?.removeChild(text);
+ this._divRef?.firstChild?.appendChild(text);
+ this.resetCursor();
}
+ this.fitTextToBox(this._divRef);
})}
+ onFocus={() => {
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, this.dataDoc);
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this._divRef?.addEventListener('focusout', this.keepFocus);
+ }}
onBlur={() => {
- this.dataDoc[this.fieldKey] = this._divRef?.innerText ?? '';
+ this._divRef?.removeEventListener('focusout', this.keepFocus);
+ this.setText(this._divRef?.innerText ?? '');
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
+ FormattedTextBox.LiveTextUndo?.end();
+ FormattedTextBox.LiveTextUndo = undefined;
}}
- contentEditable={this._props.onClickScript?.() ? false : true}
+ dangerouslySetInnerHTML={{
+ __html: `<span class="textFitted textFitAlignVert" style="display: inline-block; text-align: center; font-size: 100px; height: 0px;">${this.Title.startsWith('#') ? null : (this.Title ?? '')}</span>`,
+ }}
+ contentEditable={this._props.onClickScript?.() ? undefined : true}
ref={r => {
+ this._divRef?.removeEventListener('beforeinput', this.beforeInput);
this._divRef = r;
- this.fitTextToBox(r);
- if (this._props.isSelected() && this._divRef) {
- const range = document.createRange();
- range.setStart(this._divRef, this._divRef.childNodes.length);
- range.setEnd(this._divRef, this._divRef.childNodes.length);
- const sel = window.getSelection();
- sel?.removeAllRanges();
- sel?.addRange(range);
+ if (this._divRef) {
+ this._divRef.addEventListener('beforeinput', this.beforeInput);
+
+ if (DocumentView.SelectOnLoad === this.Document) {
+ DocumentView.SetSelectOnLoad(undefined);
+ this._divRef.focus();
+ }
+ this.fitTextToBox(this._divRef);
+ if (this.Title) {
+ this.resetCursor();
+ }
}
- }}>
- {label}
- </div>
+ }}
+ />
</div>
</div>
);
@@ -165,9 +250,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
Docs.Prototypes.TemplateMap.set(DocumentType.LABEL, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _singleLine: true, _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});
Docs.Prototypes.TemplateMap.set(DocumentType.BUTTON, {
layout: { view: LabelBox, dataField: 'title' },
- options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true },
+ options: { acl: '', _layout_nativeDimEditable: true, _layout_reflowHorizontal: true, _layout_reflowVertical: true, title_align: 'center', title_transform: 'uppercase' },
});