mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
initial set up for styling updates; actually add files
This commit is contained in:
parent
e5d8087979
commit
ebc852b430
131
webroot/js/chat/content-editable.js
Normal file
131
webroot/js/chat/content-editable.js
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { html, Component } from "https://unpkg.com/htm/preact/index.mjs?module";
|
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 Chat from './chat.js';
|
||||||
import Websocket from '../websocket.js';
|
import Websocket from '../websocket.js';
|
||||||
|
|
||||||
@ -37,17 +37,19 @@ export default class StandaloneChat extends Component {
|
|||||||
return (
|
return (
|
||||||
html`
|
html`
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<${UserInfo}
|
<${UsernameForm}
|
||||||
username=${username}
|
username=${username}
|
||||||
userAvatarImage=${userAvatarImage}
|
userAvatarImage=${userAvatarImage}
|
||||||
handleUsernameChange=${this.handleUsernameChange}
|
handleUsernameChange=${this.handleUsernameChange}
|
||||||
handleChatToggle=${this.handleChatToggle}
|
handleChatToggle=${this.handleChatToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<${Chat}
|
<${Chat}
|
||||||
websocket=${websocket}
|
websocket=${websocket}
|
||||||
username=${username}
|
username=${username}
|
||||||
userAvatarImage=${userAvatarImage}
|
userAvatarImage=${userAvatarImage}
|
||||||
chatEnabled />
|
chatEnabled
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,7 @@ const html = htm.bind(h);
|
|||||||
import { generateAvatar, setLocalStorage } from '../utils.js';
|
import { generateAvatar, setLocalStorage } from '../utils.js';
|
||||||
import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js';
|
import { KEY_USERNAME, KEY_AVATAR } from '../utils/chat.js';
|
||||||
|
|
||||||
|
export default class UsernameForm extends Component {
|
||||||
export default class UserInfo extends Component {
|
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
@ -83,25 +82,28 @@ export default class UserInfo extends Component {
|
|||||||
<img
|
<img
|
||||||
src=${userAvatarImage}
|
src=${userAvatarImage}
|
||||||
alt=""
|
alt=""
|
||||||
|
id="username-avatar"
|
||||||
class="rounded-full bg-black bg-opacity-50 border border-solid border-gray-700"
|
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>
|
||||||
|
|
||||||
<div id="user-info-change" style=${styles.form}>
|
<div id="user-info-change" style=${styles.form}>
|
||||||
<input type="text"
|
<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"
|
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"
|
maxlength="100"
|
||||||
placeholder="Update username"
|
placeholder="Update username"
|
||||||
value=${username}
|
value=${username}
|
||||||
onKeydown=${this.handleKeydown}
|
onKeydown=${this.handleKeydown}
|
||||||
ref=${this.textInput}
|
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 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 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-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>
|
||||||
</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>
|
</div>
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// DELETE THIS FILE LATER.
|
||||||
|
|
||||||
Vue.component('owncast-footer', {
|
Vue.component('owncast-footer', {
|
||||||
props: {
|
props: {
|
||||||
appVersion: {
|
appVersion: {
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
// DELETE THIS FILE LATER.
|
||||||
|
|
||||||
import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js';
|
import SOCKET_MESSAGE_TYPES from './utils/socket-message-types.js';
|
||||||
|
|
||||||
const KEY_USERNAME = 'owncast_username';
|
const KEY_USERNAME = 'owncast_username';
|
||||||
|
|||||||
@ -4,6 +4,11 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"/>
|
<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="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>
|
<script src="//unpkg.com/showdown/dist/showdown.min.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -165,3 +165,5 @@ But really it's just the innerHTML content.
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Overall layout styles for all of owncast app.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
/* variables */
|
/* variables */
|
||||||
:root {
|
:root {
|
||||||
--header-height: 3.5em;
|
--header-height: 3.5em;
|
||||||
|
|||||||
4
webroot/styles/standalone-chat.css
Normal file
4
webroot/styles/standalone-chat.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/*
|
||||||
|
The styles in this file mostly ovveride those coming fro chat.css
|
||||||
|
|
||||||
|
*/
|
||||||
Loading…
x
Reference in New Issue
Block a user