diff --git a/src/util/string-manipulation.js b/src/util/string-manipulation.js index 6afde59e5..8a7ffc69c 100644 --- a/src/util/string-manipulation.js +++ b/src/util/string-manipulation.js @@ -76,3 +76,57 @@ export const truncateString = (fullStr, strLen, separator = ' ... ') => { * @returns { string } - capitalized string */ export const capitalize = s => (s?.[0]?.toUpperCase() + s?.slice(1)) || '' + +// "vendored" from https://github.com/mdevils/html-entities/blob/68a1a96/src/xml-entities.ts +const decodeXML = (str) => { + const ALPHA_INDEX = { + '<': '<', + '>': '>', + '"': '"', + '&apos': '\'', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', + '&': '&', + } + if (!str || !str.length) { + return '' + } + return str.replace(/&#?[0-9a-zA-Z]+;?/g, (s) => { + if (s.charAt(1) === '#') { + const code = s.charAt(2).toLowerCase() === 'x' ? + parseInt(s.substr(3), 16) : + parseInt(s.substr(2)) + + if (isNaN(code) || code < -32768 || code > 65535) { + return '' + } + return String.fromCharCode(code) + } + return ALPHA_INDEX[s] || s + }) +} + +/** + * https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/50813259#50813259 + * getTextSize - calculates a rendered text width and height in rem + * @param { string } text - a text string + * @param { number || string } fontWeight - text's font weight + * @param { number } fontSize - text's font size in pixels + * @param { string } fontFamily - text's font family + * @returns { object } - the width and height of the rendered text in rem + */ +export const getTextSize = (text, fontWeight, fontSize, fontFamily) => { + let font = `${fontWeight} ${fontSize}px ${fontFamily}` + let canvas = document.createElement('canvas') + let context = canvas.getContext('2d') + context.font = font + let metrics = typeof text === 'number' + ? context.measureText(text) + : context.measureText(decodeXML(text)) + return { + width: metrics.width, + } +} diff --git a/src/view/title-bar/editable-title.js b/src/view/title-bar/editable-title.js index c4c1a8f4f..6d435113a 100644 --- a/src/view/title-bar/editable-title.js +++ b/src/view/title-bar/editable-title.js @@ -1,10 +1,10 @@ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' -import { Icons, TextField, makeStyles, getTailwindConfigColor } from '@eqworks/lumen-labs' +import { TextField, makeStyles, getTailwindConfigColor } from '@eqworks/lumen-labs' import { useStoreState, useStoreActions } from '../../store' -import CustomButton from '../../components/custom-button' import modes from '../../constants/modes' +import { getTextSize } from '../../util/string-manipulation' const commonClasses = { @@ -13,6 +13,24 @@ const commonClasses = { alignItems: 'center', margin: '0 0.6rem', display: 'flex', + + '& .textfield-container': { + backgroundColor: getTailwindConfigColor('secondary-100'), + + '& .textfield-root': { + borderTop: 0, + + '& .textfield-input': { + fontFamily: 'Open Sans', + outlineWidth: 0, + outlineColor: 'transparent', + }, + }, + + '& .textfield-root:focus-within': { + borderColor: getTailwindConfigColor('primary-500'), + }, + }, }, button: { marginLeft: '0.4rem', @@ -29,100 +47,102 @@ const commonClasses = { }, } -const useStyles = (mode) => makeStyles( - mode === modes.EDITOR - ? { - title: { - display: 'flex', - alignItems: 'center', - color: getTailwindConfigColor('secondary-600'), - fontWeight: 700, - background: getTailwindConfigColor('secondary-100'), - fontSize: '0.875rem', - padding: '0.2rem 0.4rem', - paddingLeft: '0.6rem', - cursor: 'default', - }, - ...commonClasses, - } - : { - title: { - color: getTailwindConfigColor('primary-500'), - fontSize: '1.125rem', - fontWeight: 700, - }, - ...commonClasses, - } +const useStyles = () => makeStyles( + { + title: { + color: getTailwindConfigColor('primary-500'), + fontSize: '1.125rem', + fontWeight: 700, + }, + ...commonClasses, + }, ) +const textFieldClasses = Object.freeze({ + container: 'textfield-container', + root: 'textfield-root', + input: 'textfield-input mb-0 text-sm focus:text-secondary-900', +}) + const EditableTitle = () => { const userUpdate = useStoreActions((actions) => actions.userUpdate) const isLoading = useStoreState((state) => state.isLoading) const title = useStoreState((state) => state.title) const mode = useStoreState((state) => state.ui.mode) - const classes = useStyles(mode) + const classes = useStyles() const [editing, setEditing] = useState(false) const [tentativeTitle, setTentativeTitle] = useState(title) + + const textfieldRef = useRef(null) + useEffect(() => { setTentativeTitle(title) }, [title]) - const renderEditButton = ( - setEditing(true)} - endIcon={} - /> - ) - const renderTitle = ( -
+
{isLoading ? '...' : title} - { - (mode === modes.QL || mode === modes.EDITOR) && - renderEditButton - }
) + const renderTextfield = () => { + if (textfieldRef.current) { + let el = textfieldRef.current.childNodes[0][0] + const lengthSize = el.value.length > 12 ? 14 : 16 + el.style.width = `${getTextSize(el.value, 700, lengthSize, 'Open Sans').width}px` + } + + return ( + { + setTentativeTitle(v) + }} + onBlur={(e) => { + e.preventDefault() + e.stopPropagation() + e.nativeEvent.preventDefault() + e.nativeEvent.stopPropagation() + setEditing(false) + }} + onFocus={(e) => { + e.preventDefault() + e.stopPropagation() + e.nativeEvent.preventDefault() + e.nativeEvent.stopPropagation() + setEditing(true) + }} + deleteButton={editing} + onDelete={(e) => { + e.preventDefault() + e.stopPropagation() + e.nativeEvent.preventDefault() + e.nativeEvent.stopPropagation() + updateTitle('') + }} + /> + ) + } + const updateTitle = title => userUpdate({ title }) return ( -
+
{ - editing - ?
{ - e.preventDefault() - e.stopPropagation() - e.nativeEvent.preventDefault() - e.nativeEvent.stopPropagation() - updateTitle(e.target.children[0].children[0].value) - setEditing(false) - }} - > - setTentativeTitle(v)} - onSubmit={(e) => { - e.nativeEvent.preventDefault() - e.nativeEvent.stopPropagation() - }} - onBlur={(e) => { - updateTitle(e.target.value) - setTentativeTitle(title) - setEditing(false) - }} - /> - - : <> + mode === modes.EDITOR + ? + <> + {renderTextfield()} + + : + <> {renderTitle} }