initial set up for styling updates; actually add files

This commit is contained in:
Ginger Wong 2020-08-17 09:00:36 -07:00
parent e5d8087979
commit ebc852b430
9 changed files with 170 additions and 15 deletions

View File

@ -0,0 +1,131 @@
/*
Since we can't really import react-contenteditable here, I'm borrowing code for this component from here:
github.com/lovasoa/react-contenteditable/
and here:
https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103
*/
import { Component, createRef, createElement } from 'https://unpkg.com/preact?module';
function replaceCaret(el) {
// Place the caret at the end of the element
const target = document.createTextNode('');
el.appendChild(target);
// do not move caret if element was not focused
const isTargetFocused = document.activeElement === el;
if (target !== null && target.nodeValue !== null && isTargetFocused) {
var sel = window.getSelection();
if (sel !== null) {
var range = document.createRange();
range.setStart(target, target.nodeValue.length);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
if (el) el.focus();
}
}
function normalizeHtml(str) {
return str && str.replace(/ |\u202F|\u00A0/g, ' ');
}
export default class ContentEditable extends Component {
constructor(props) {
super(props);
this.el = createRef();
this.lastHtml = '';
this.emitChange = this.emitChange.bind(this);
this.getDOMElement = this.getDOMElement.bind(this);
}
shouldComponentUpdate(nextProps) {
const { props } = this;
const el = this.getDOMElement();
// We need not rerender if the change of props simply reflects the user's edits.
// Rerendering in this case would make the cursor/caret jump
// Rerender if there is no element yet... (somehow?)
if (!el) return true;
// ...or if html really changed... (programmatically, not by user edit)
if (
normalizeHtml(nextProps.html) !== normalizeHtml(el.innerHTML)
) {
return true;
}
// Handle additional properties
return props.disabled !== nextProps.disabled ||
props.tagName !== nextProps.tagName ||
props.className !== nextProps.className ||
props.innerRef !== nextProps.innerRef;
}
componentDidUpdate() {
const el = this.getDOMElement();
if (!el) return;
// Perhaps React (whose VDOM gets outdated because we often prevent
// rerendering) did not update the DOM. So we update it manually now.
if (this.props.html !== el.innerHTML) {
el.innerHTML = this.props.html;
}
this.lastHtml = this.props.html;
replaceCaret(el);
}
getDOMElement() {
return (this.props.innerRef && typeof this.props.innerRef !== 'function' ? this.props.innerRef : this.el).current;
}
emitChange(originalEvt) {
const el = this.getDOMElement();
if (!el) return;
const html = el.innerHTML;
if (this.props.onChange && html !== this.lastHtml) {
// Clone event with Object.assign to avoid
// "Cannot assign to read only property 'target' of object"
const evt = Object.assign({}, originalEvt, {
target: {
value: html
}
});
this.props.onChange(evt);
}
this.lastHtml = html;
}
render(props) {
const { html, innerRef } = props;
return createElement(
'div',
{
...props,
ref: typeof innerRef === 'function' ? (current) => {
innerRef(current)
this.el.current = current
} : innerRef || this.el,
onInput: this.emitChange,
onBlur: this.props.onBlur || this.emitChange,
onKeyup: this.props.onKeyUp || this.emitChange,
onKeydown: this.props.onKeyDown || this.emitChange,
contentEditable: !this.props.disabled,
dangerouslySetInnerHTML: { __html: html },
},
this.props.children,
);
}
}

View File

@ -1,5 +1,5 @@
import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module";
import UserInfo from './user-info.js';
import UsernameForm from './username.js';
import Chat from './chat.js';
import Websocket from '../websocket.js';
@ -37,17 +37,19 @@ export default class StandaloneChat extends Component {
return (
html`
<div class="flex">
<${UserInfo}
<${UsernameForm}
username=${username}
userAvatarImage=${userAvatarImage}
handleUsernameChange=${this.handleUsernameChange}
handleChatToggle=${this.handleChatToggle}
/>
<${Chat}
websocket=${websocket}
username=${username}
userAvatarImage=${userAvatarImage}
chatEnabled />
chatEnabled
/>
</div>
`);
}

View File

@ -6,8 +6,7 @@ const html = htm.bind(h);
import { generateAvatar, setLocalStorage } from '../utils.js';
import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js';
export default class UserInfo extends Component {
export default class UsernameForm extends Component {
constructor(props, context) {
super(props, context);
@ -83,25 +82,28 @@ export default class UserInfo extends Component {
<img
src=${userAvatarImage}
alt=""
id="username-avatar"
class="rounded-full bg-black bg-opacity-50 border border-solid border-gray-700"
/>
<span class="text-indigo-600">${username}</span>
<span id="username-display" class="text-indigo-600">${username}</span>
</div>
<div id="user-info-change" style=${styles.form}>
<input type="text"
id="username-change-input"
class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-1 px-1 leading-tight focus:bg-white"
maxlength="100"
placeholder="Update username"
value=${username}
onKeydown=${this.handleKeydown}
ref=${this.textInput}
>
<button onClick=${this.handleUpdateUsername} class="bg-blue-500 hover:bg-blue-700 text-white py-1 px-1 rounded user-btn">Update</button>
<button onClick=${this.handleHideForm} class="bg-gray-900 hover:bg-gray-800 py-1 px-2 rounded user-btn text-white text-opacity-50" title="cancel">X</button>
/>
<button id="button-update-username" onClick=${this.handleUpdateUsername} class="bg-blue-500 hover:bg-blue-700 text-white py-1 px-1 rounded user-btn">Update</button>
<button id="button-cancel-change" onClick=${this.handleHideForm} class="bg-gray-900 hover:bg-gray-800 py-1 px-2 rounded user-btn text-white text-opacity-50" title="cancel">X</button>
</div>
</div>
<button type="button" onClick=${handleChatToggle} class="flex bg-gray-800 hover:bg-gray-700">💬</button>
<button type="button" id="chat-toggle" onClick=${handleChatToggle} class="flex bg-gray-800 hover:bg-gray-700">💬</button>
</div>
`);
}

View File

@ -1,3 +1,5 @@
// DELETE THIS FILE LATER.
Vue.component('owncast-footer', {
props: {
appVersion: {
@ -5,7 +7,7 @@ Vue.component('owncast-footer', {
default: '0.1',
},
},
template: `
<footer class="flex">
<span>
@ -24,7 +26,7 @@ Vue.component('stream-tags', {
class="tag-list flex"
v-if="this.tags.length"
>
<li class="tag rounded-sm text-gray-100 bg-gray-700"
<li class="tag rounded-sm text-gray-100 bg-gray-700"
v-for="tag in this.tags"
v-bind:key="tag"
>
@ -43,9 +45,9 @@ Vue.component('user-details', {
v-bind:style="{ backgroundImage: 'url(' + logo + ')' }"
>
<img
class="logo visually-hidden"
alt="Logo"
v-bind:src="logo">
class="logo visually-hidden"
alt="Logo"
v-bind:src="logo">
</div>
<div class="user-content-header border-b border-gray-500 border-solid">
<h2 class="font-semibold">

View File

@ -1,3 +1,5 @@
// DELETE THIS FILE LATER.
import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js';
const KEY_USERNAME = 'owncast_username';

View File

@ -4,6 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet" />
<link href="./styles/layout.css" rel="stylesheet" />
<link href="./styles/chat.css" rel="stylesheet" />
<link href="./styles/standalone-chat.css" rel="stylesheet" />
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
</head>

View File

@ -165,3 +165,5 @@ But really it's just the innerHTML content.
opacity: 0.6;
}

View File

@ -1,3 +1,8 @@
/*
Overall layout styles for all of owncast app.
*/
/* variables */
:root {
--header-height: 3.5em;

View File

@ -0,0 +1,4 @@
/*
The styles in this file mostly ovveride those coming fro chat.css
*/