From 7a1512ef6b00081c1d6e9fda926978e6fef5d226 Mon Sep 17 00:00:00 2001 From: Ginger Wong Date: Thu, 13 Aug 2020 02:43:41 -0700 Subject: [PATCH] breaking out styles into smaller files; break out chat helper functions into utils --- webroot/js/chat/chat.js | 1 + webroot/js/utils/chat.js | 162 ++++++++++++-- webroot/styles/layout.css | 362 +------------------------------- webroot/styles/message.css | 86 +++++++- webroot/styles/user-content.css | 75 ++++++- 5 files changed, 314 insertions(+), 372 deletions(-) diff --git a/webroot/js/chat/chat.js b/webroot/js/chat/chat.js index bc000bade..23cfa3b4d 100644 --- a/webroot/js/chat/chat.js +++ b/webroot/js/chat/chat.js @@ -74,6 +74,7 @@ export default class Chat extends Component { placeholder="Message" class="appearance-none block w-full bg-gray-200 text-gray-700 border border-black-500 rounded py-2 px-2 my-2 focus:bg-white" > +
😏
diff --git a/webroot/js/utils/chat.js b/webroot/js/utils/chat.js index 939b072c5..dc26d23ca 100644 --- a/webroot/js/utils/chat.js +++ b/webroot/js/utils/chat.js @@ -8,7 +8,7 @@ export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.'; export function formatMessageText(message) { showdown.setFlavor('github'); - var markdownToHTML = new showdown.Converter({ + let formattedText = new showdown.Converter({ emoji: true, openLinksInNewWindow: true, tables: false, @@ -16,20 +16,158 @@ export function formatMessageText(message) { literalMidWordUnderscores: true, strikethrough: true, ghMentions: false, - }).makeHtml(this.body); - const linked = autoLink(markdownToHTML, { - embed: true, - removeHTTP: true, - linkAttr: { - target: '_blank' - } - }); - const highlighted = highlightUsername(linked); - return addNewlines(highlighted); + }).makeHtml(message); + + formattedText = linkify(formattedText, message); + formattedText = highlightUsername(formattedText); + + return addNewlines(formattedText); } function highlightUsername(message) { const username = document.getElementById('self-message-author').value; - const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); + const pattern = new RegExp('@?' + username.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi'); return message.replace(pattern, '$&'); } + +function linkify(text, rawText) { + const urls = getURLs(stripTags(rawText)); + if (urls) { + urls.forEach(function (url) { + let linkURL = url; + + // Add http prefix if none exist in the URL so it actually + // will work in an anchor tag. + if (linkURL.indexOf('http') === -1) { + linkURL = 'http://' + linkURL; + } + + // Remove the protocol prefix in the display URLs just to make + // things look a little nicer. + const displayURL = url.replace(/(^\w+:|^)\/\//, ''); + const link = `${displayURL}`; + text = text.replace(url, link); + + if (getYoutubeIdFromURL(url)) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += '
'; + } + + const youtubeID = getYoutubeIdFromURL(url); + text += getYoutubeEmbedFromID(youtubeID); + } else if (url.indexOf('instagram.com/p/') > -1) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += `
`; + } + text += getInstagramEmbedFromURL(url); + } else if (isImage(url)) { + if (isTextJustURLs(text, [url, displayURL])) { + text = ''; + } else { + text += `
`; + } + text += getImageForURL(url); + } + }.bind(this)); + } + return text; +} + +function isTextJustURLs(text, urls) { + for (var i = 0; i < urls.length; i++) { + const url = urls[i]; + if (stripTags(text) === url) { + return true; + } + } + return false; +} + + +function stripTags(str) { + return str.replace(/<\/?[^>]+(>|$)/g, ""); +} + +function getURLs(str) { + var exp = /((\w+:\/\/\S+)|(\w+[\.:]\w+\S+))[^\s,\.]/ig; + return str.match(exp); +} + +function getYoutubeIdFromURL(url) { + try { + var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; + var match = url.match(regExp); + + if (match && match[2].length == 11) { + return match[2]; + } else { + return null; + } + } catch (e) { + console.log(e); + return null; + } +} + +function getYoutubeEmbedFromID(id) { + return ``; +} + +function getInstagramEmbedFromURL(url) { + const urlObject = new URL(url.replace(/\/$/, "")); + urlObject.pathname += "/embed"; + return ``; +} + +function isImage(url) { + const re = /\.(jpe?g|png|gif)$/; + const isImage = re.test(url); + return isImage; +} + +function getImageForURL(url) { + return ``; +} + + +// Taken from https://stackoverflow.com/questions/3972014/get-contenteditable-caret-index-position +export function getCaretPosition(editableDiv) { + var caretPos = 0, + sel, range; + if (window.getSelection) { + sel = window.getSelection(); + if (sel.rangeCount) { + range = sel.getRangeAt(0); + if (range.commonAncestorContainer.parentNode == editableDiv) { + caretPos = range.endOffset; + } + } + } else if (document.selection && document.selection.createRange) { + range = document.selection.createRange(); + if (range.parentElement() == editableDiv) { + var tempEl = document.createElement("span"); + editableDiv.insertBefore(tempEl, editableDiv.firstChild); + var tempRange = range.duplicate(); + tempRange.moveToElementText(tempEl); + tempRange.setEndPoint("EndToEnd", range); + caretPos = tempRange.text.length; + } + } + return caretPos; +} + +// Pieced together from parts of https://stackoverflow.com/questions/6249095/how-to-set-caretcursor-position-in-contenteditable-element-div +export function setCaretPosition(editableDiv, position) { + var range = document.createRange(); + var sel = window.getSelection(); + range.selectNode(editableDiv); + range.setStart(editableDiv.childNodes[0], position); + range.collapse(true); + + sel.removeAllRanges(); + sel.addRange(range); +} diff --git a/webroot/styles/layout.css b/webroot/styles/layout.css index b179dfef1..611408072 100644 --- a/webroot/styles/layout.css +++ b/webroot/styles/layout.css @@ -23,9 +23,9 @@ a:hover { } -.visually-hidden { +.visually-hidden { position: absolute !important; - height: 1px; + height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ @@ -111,7 +111,7 @@ footer span { flex-direction: row; justify-content: space-between; - + } #stream-info span { font-size: .7em; @@ -128,23 +128,7 @@ footer span { /* ************************************************8 */ -.user-content { - padding: 3em; - display: flex; - flex-direction: row; -} -.user-content .user-image { - padding: 1em; - margin-right: 2em; - min-width: var(--user-image-width); - width: var(--user-image-width); - height: var(--user-image-width); - max-height: var(--user-image-width); - background-repeat: no-repeat; - background-position: center center; - background-size: calc(var(--user-image-width) - 1em); -} /* .user-image img { display: inline-block; @@ -156,58 +140,7 @@ footer span { } h2 { - font-size: 3em; -} -.user-content-header { - margin-bottom: 2em; -} - -.tag-list { - flex-direction: row; - margin: 1em 0; -} -.tag-list li { - font-size: .75em; - text-transform: uppercase; - margin-right: .75em; - padding: .5em; -} - - -.social-list { - flex-direction: row; - align-items: center; - justify-content: flex-start; - flex-wrap: wrap; -} -.social-list .follow-label { - font-weight: bold; - font-size: .75em; - margin-right: .5em; - text-transform: uppercase; -} - -.user-social-item { - display: flex; - justify-content: flex-start; - align-items: center; - margin-right: -.25em; -} -.user-social-item .platform-icon { - --icon-width: 40px; - height: var(--icon-width); - width: var(--icon-width); - background-image: url(../img/social-icons.gif); - background-repeat: no-repeat; - background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); - transform: scale(.65); -} - -.user-social-item.use-default .platform-label { - font-size: .7em; - text-transform: uppercase; - display: inline-block; - max-width: 10em; + font-size: 3em; } @@ -281,8 +214,8 @@ h2 { margin-top: var(--header-height); background-position: center center; background-repeat: no-repeat; - - background-size: 30%; + + background-size: 30%; } .owncast-video-container { @@ -360,81 +293,6 @@ h2 { } -#messages-container { - overflow: auto; - padding: 1em 0; -} -#message-input-container { - width: 100%; - padding: 1em; -} - -#message-form { - flex-direction: column; - align-items: flex-end; - margin-bottom: 0; -} -#message-body-form { - font-size: 1em; - height: 60px; -} -#message-body-form:disabled{ - opacity: .5; -} -#message-body-form img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} - -#message-body-form .emoji { - width: 40px; -} - -#message-form-actions { - flex-direction: row; - justify-content: space-between; - align-items: center; - width: 100%; -} - -.message-text img { - display: inline; - padding-left: 5px; - padding-right: 5px; -} - -.message-text .emoji { - width: 60px; -} - -/* ************************************************8 */ - -.message { - padding: .85em; - align-items: flex-start; -} -.message-avatar { - margin-right: .75em; -} -.message-avatar img { - max-width: unset; - height: 3.0em; - width: 3.0em; - padding: 5px; -} - -.message-content { - font-size: .85em; - max-width: 85%; - word-wrap: break-word; -} -.message-content a { - color: #7F9CF5; /* indigo-400 */ -} -.message-content a:hover { - text-decoration: underline; -} /* ************************************************8 */ @@ -452,7 +310,7 @@ h2 { --right-col-width: 20em; --user-image-width: 6em; } - + #chat-container { width: var(--right-col-width); } @@ -504,21 +362,6 @@ h2 { } } -/* try not making the video fixed position for now */ -@media (min-height: 861px) { - /* main { - position: fixed; - z-index: 9; - width: 100%; - } - #user-content { - margin-top: calc(var(--video-container-height) + var(--header-height) + 2em) - } */ -} - - - - @@ -530,194 +373,3 @@ h2 { flex-direction: column; } } - -.extra-user-content { - padding: 1em 3em 3em 3em; -} - -.extra-user-content ol { - list-style: decimal; -} - -.extra-user-content ul { - list-style: unset; -} - -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4 { - color: #111111; - font-weight: 400; } - -.extra-user-content h1, .extra-user-content h2, .extra-user-content h3, .extra-user-content h4, .extra-user-content h5, .extra-user-content p { - margin-bottom: 24px; - padding: 0; } - -.extra-user-content h1 { - font-size: 48px; } - -.extra-user-content h2 { - font-size: 36px; - margin: 24px 0 6px; } - -.extra-user-content h3 { - font-size: 24px; } - -.extra-user-content h4 { - font-size: 21px; } - -.extra-user-content h5 { - font-size: 18px; } - -.extra-user-content a { - color: #0099ff; - margin: 0; - padding: 0; - vertical-align: baseline; } - -.extra-user-content ul, .extra-user-content ol { - padding: 0; - margin: 0; } - -.extra-user-content li { - line-height: 24px; } - -.extra-user-content li ul, .extra-user-content li ul { - margin-left: 24px; } - -.extra-user-content p, .extra-user-content ul, .extra-user-content ol { - font-size: 16px; - line-height: 24px; - } - -.extra-user-content pre { - padding: 0px 24px; - max-width: 800px; - white-space: pre-wrap; } - -.extra-user-content code { - font-family: Consolas, Monaco, Andale Mono, monospace; - line-height: 1.5; - font-size: 13px; } - -.extra-user-content aside { - display: block; - float: right; - width: 390px; } - -.extra-user-content blockquote { - margin: 1em 2em; - max-width: 476px; } - -.extra-user-content blockquote p { - color: #666; - max-width: 460px; } - -.extra-user-content hr { - width: 540px; - text-align: left; - margin: 0 auto 0 0; - color: #999; } - -.extra-user-content table { - border-collapse: collapse; - margin: 1em 1em; - border: 1px solid #CCC; } - -.extra-user-content table thead { - background-color: #EEE; } - -.extra-user-content table thead td { - color: #666; } - -.extra-user-content table td { - padding: 0.5em 1em; - border: 1px solid #CCC; } - -.message-text iframe { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text .instagram-embed { - height: 314px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} - -.message-text .embedded-image { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} - -/* Emoji picker */ -#emoji-button { - position: relative; - top: -65px; - right: 10px; - cursor: pointer; -} -.message-text .embedded-image { - width: 100%; - height: 170px; - border-radius: 15px; -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} -.message-text .highlighted { - color: orange; - font-weight: 400; - font-size: 14px; - -} - -.message-text code { - background-color:darkslategrey; - padding: 3px; -} - -/* -The chat input has a fake placeholder that is styled below. -It pulls the placeholder text from the div's placeholder attribute. -But really it's just the innerHTML content. -*/ - -/* If the div is empty then show the placeholder */ -#message-body-form:empty:before{ - content: attr(placeholder); - pointer-events: none; - display: block; /* For Firefox */ - - /* Style the div's placeholder text color */ - color: rgba(0, 0, 0, 0.5); -} - -/* When chat is enabled (contenteditable=true) */ -#message-body-form[contenteditable=true]:before { - opacity: 1.0; -} - - -/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ -#message-body-form[contenteditable=false] { - opacity: 0.6; -} - diff --git a/webroot/styles/message.css b/webroot/styles/message.css index 67b7b77d6..1dd2021ef 100644 --- a/webroot/styles/message.css +++ b/webroot/styles/message.css @@ -48,6 +48,7 @@ } + .message { padding: .85em; align-items: flex-start; @@ -74,14 +75,93 @@ text-decoration: underline; } + .message-text iframe { width: 100%; height: 170px; border-radius: 15px; } +.message-text .instagram-embed { + height: 314px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} /* Emoji picker */ #emoji-button { - margin: 0 .5em; - font-size: 1.5em -} \ No newline at end of file + position: relative; + top: -65px; + right: 10px; + cursor: pointer; +} + +.message-text .embedded-image { + width: 100%; + height: 170px; + border-radius: 15px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} + +/* Emoji picker */ +#emoji-button { + position: relative; + top: -65px; + right: 10px; + cursor: pointer; +} +.message-text .embedded-image { + width: 100%; + height: 170px; + border-radius: 15px; +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} +.message-text .highlighted { + color: orange; + font-weight: 400; + font-size: 14px; + +} + +.message-text code { + background-color:darkslategrey; + padding: 3px; +} + +/* +The chat input has a fake placeholder that is styled below. +It pulls the placeholder text from the div's placeholder attribute. +But really it's just the innerHTML content. +*/ + +/* If the div is empty then show the placeholder */ +#message-body-form:empty:before{ + content: attr(placeholder); + pointer-events: none; + display: block; /* For Firefox */ + + /* Style the div's placeholder text color */ + color: rgba(0, 0, 0, 0.5); +} + +/* When chat is enabled (contenteditable=true) */ +#message-body-form[contenteditable=true]:before { + opacity: 1.0; +} + + +/* When chat is disabled (contenteditable=false) chat input div should appear disabled. */ +#message-body-form[contenteditable=false] { + opacity: 0.6; +} + diff --git a/webroot/styles/user-content.css b/webroot/styles/user-content.css index 7a04ee539..dea858f08 100644 --- a/webroot/styles/user-content.css +++ b/webroot/styles/user-content.css @@ -1,8 +1,79 @@ -.extra-user-content { - padding: 1em 3em 3em 3em; +.user-content { + padding: 3em; + + display: flex; + flex-direction: row; +} +.user-content .user-image { + padding: 1em; + margin-right: 2em; + min-width: var(--user-image-width); + width: var(--user-image-width); + height: var(--user-image-width); + max-height: var(--user-image-width); + background-repeat: no-repeat; + background-position: center center; + background-size: calc(var(--user-image-width) - 1em); +} + +.user-content-header { + margin-bottom: 2em; +} + +.tag-list { + flex-direction: row; + margin: 1em 0; +} +.tag-list li { + font-size: .75em; + text-transform: uppercase; + margin-right: .75em; + padding: .5em; } +.social-list { + flex-direction: row; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; +} +.social-list .follow-label { + font-weight: bold; + font-size: .75em; + margin-right: .5em; + text-transform: uppercase; +} + +.user-social-item { + display: flex; + justify-content: flex-start; + align-items: center; + margin-right: -.25em; +} +.user-social-item .platform-icon { + --icon-width: 40px; + height: var(--icon-width); + width: var(--icon-width); + background-image: url(../img/social-icons.gif); + background-repeat: no-repeat; + background-position: calc(var(--imgCol) * var(--icon-width)) calc(var(--imgRow) * var(--icon-width)); + transform: scale(.65); +} + +.user-social-item.use-default .platform-label { + font-size: .7em; + text-transform: uppercase; + display: inline-block; + max-width: 10em; +} + + + + +.extra-user-content { + padding: 1em 3em 3em 3em; +} .extra-user-content ol { list-style: decimal;