WIP: Shadow DOM

This commit is contained in:
Manuel Bouza
2019-02-19 15:32:53 +01:00
parent dd4fa996e8
commit a9007b825e
21 changed files with 251 additions and 220 deletions

View File

@@ -5,6 +5,7 @@
"main": "bundle.js", "main": "bundle.js",
"scripts": { "scripts": {
"start": "node_modules/.bin/webpack --watch", "start": "node_modules/.bin/webpack --watch",
"build": "node_modules/.bin/webpack",
"test": "node_modules/.bin/jest", "test": "node_modules/.bin/jest",
"test:watch": "node_modules/.bin/jest --watch", "test:watch": "node_modules/.bin/jest --watch",
"release": "copyfiles main.css main.min.js background.min.js manifest.json popup.html options.html node_modules/jquery/dist/jquery.min.js node_modules/select2/select2.js src/images/* release" "release": "copyfiles main.css main.min.js background.min.js manifest.json popup.html options.html node_modules/jquery/dist/jquery.min.js node_modules/select2/select2.js src/images/* release"
@@ -20,6 +21,7 @@
"react": "^16.8.0", "react": "^16.8.0",
"react-dom": "^16.8.0", "react-dom": "^16.8.0",
"react-select": "^2.3.0", "react-select": "^2.3.0",
"react-shadow": "^16.3.2",
"route-parser": "^0.0.5" "route-parser": "^0.0.5"
}, },
"devDependencies": { "devDependencies": {
@@ -42,6 +44,7 @@
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0", "jest": "^24.1.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"prettier": "^1.16.4", "prettier": "^1.16.4",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",

View File

@@ -1,93 +1,91 @@
#moco-bx-container { input {
input { border-radius: 0;
border-radius: 0; }
.form-group {
width: 100%;
margin: 1rem 0;
label {
display: block;
font-weight: bold;
margin-bottom: 0.25rem;
} }
.form-group { input, textarea {
padding: 0.25rem 0.5rem;
background-color: white;
border-color: #cccccc;
width: 100%; width: 100%;
margin: 1rem 0; }
label { text-muted {
display: block; color: #eee;
font-weight: bold; }
margin-bottom: 0.25rem;
}
&.has-error {
input, textarea { input, textarea {
padding: 0.25rem 0.5rem; border-color: #FB3A2F;
background-color: white;
border-color: #cccccc;
width: 100%;
} }
}
text-muted { .form-error {
color: #eee; color: #FB3A2F;
} }
&.has-error { .input-group {
input, textarea { position: relative;
border-color: #FB3A2F; display: table;
} border-collapse: separate;
}
.form-error { input {
color: #FB3A2F;
}
.input-group {
position: relative; position: relative;
display: table; display: table-cell;
border-collapse: separate;
input {
position: relative;
display: table-cell;
}
.input-group-addon {
padding: 0.25rem 0.5rem;
font-weight: normal;
color: #555555;
text-align: center;
background-color: #eeeeee;
border: 1px solid #cccccc;
border-left: none;
display: table-cell;
width: 1%;
}
}
}
input[name="hours"] {
width: 33%;
}
textarea[name="description"] {
resize: none;
width: 100%;
}
button {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-weight: normal;
text-align: center;
white-space: nowrap;
color: white;
background-image: none;
background-color: #7dc332;
border-color: #7dc332;
cursor: pointer;
&:hover:not(:disabled) {
background-color: #639a28;
border-color: #639a28;
} }
&:disabled { .input-group-addon {
opacity: 0.65; padding: 0.25rem 0.5rem;
cursor: default; font-weight: normal;
color: #555555;
text-align: center;
background-color: #eeeeee;
border: 1px solid #cccccc;
border-left: none;
display: table-cell;
width: 1%;
} }
} }
} }
input[name="hours"] {
width: 33%;
}
textarea[name="description"] {
resize: none;
width: 100%;
}
button {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-weight: normal;
text-align: center;
white-space: nowrap;
color: white;
background-image: none;
background-color: #7dc332;
border-color: #7dc332;
cursor: pointer;
&:hover:not(:disabled) {
background-color: #639a28;
border-color: #639a28;
}
&:disabled {
opacity: 0.65;
cursor: default;
}
}

45
src/css/_reset.scss Normal file
View File

@@ -0,0 +1,45 @@
#moco-bx-bubble, #moco-bx-container {
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
}

View File

@@ -1,11 +1,11 @@
#moco-bx-bubble, #moco-bx-container { #moco-bx-bubble, #moco-bx-container {
@keyframes spinner { @keyframes moco-bx-spinner {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.spinner { .moco-bx-spinner {
display: inline-block; display: inline-block;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
@@ -13,6 +13,6 @@
border: 2px solid #999; border: 2px solid #999;
border-right-color: transparent; border-right-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: spinner .75s linear infinite; animation: moco-bx-spinner .75s linear infinite;
} }
} }

31
src/css/content.scss Normal file
View File

@@ -0,0 +1,31 @@
@import "mixins";
@import "form";
@import "spinner";
.moco-bx-bubble {
img {
width: 30px;
height: 30px;
}
}
.moco-bx-modal {
position: fixed; /* Stay in place */
z-index: 2000; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
.moco-bx-modal-content {
background-color: white;
width: 600px;
padding: 40px;
margin: 0 auto;
}
}

View File

@@ -1,48 +0,0 @@
@import "mixins";
@import "form";
@import "spinner";
#moco-bx-bubble {
position: fixed;
bottom: 40px;
left: 50%;
margin-left: -30px;
z-index: 1000;
height: 60px;
width: 60px;
background-color: white;
border-radius: 50%;
box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05),
2px 2px 15px 4px rgba(0, 0, 0, 0.05);
padding: 5px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
cursor: pointer;
}
#moco-bx-container {
.moco-bx-modal {
position: fixed; /* Stay in place */
z-index: 2000; /* Sit on top */
padding-top: 100px; /* Location of the box */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0, 0, 0); /* Fallback color */
background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */
}
.moco-bx-modal-content {
background-color: white;
width: 600px;
padding: 40px;
margin: 0 auto;
}
}

0
src/css/options.scss Normal file
View File

22
src/css/styles.css Normal file
View File

@@ -0,0 +1,22 @@
#moco-bx-root {
position: fixed;
bottom: 40px;
left: 50%;
margin-left: -30px;
z-index: 1000;
height: 60px;
width: 60px;
background-color: white;
border-radius: 50%;
box-shadow: -1px -1px 15px 4px rgba(0, 0, 0, 0.05),
2px 2px 15px 4px rgba(0, 0, 0, 0.05);
padding: 5px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
cursor: pointer;
}

View File

@@ -1,7 +1,7 @@
import React, { Component } from "react" import React, { Component } from "react"
import PropTypes from "prop-types" import PropTypes from "prop-types"
import ApiClient from "api/Client" import ApiClient from "api/Client"
import Modal, { Content } from "components/Modal" import Modal from "components/Modal"
import InvalidConfigurationError from "components/InvalidConfigurationError" import InvalidConfigurationError from "components/InvalidConfigurationError"
import Form from "components/Form" import Form from "components/Form"
import Spinner from "components/Spinner" import Spinner from "components/Spinner"
@@ -160,6 +160,7 @@ class Bubble extends Component {
// EVENT HANDLERS ----------------------------------------------------------- // EVENT HANDLERS -----------------------------------------------------------
handleKeyDown = event => { handleKeyDown = event => {
event.stopPropagation()
if (event.keyCode === 27) { if (event.keyCode === 27) {
this.close() this.close()
} }
@@ -223,19 +224,18 @@ class Bubble extends Component {
} }
return ( return (
<> <div className="moco-bx-bubble">
<img <img
onClick={this.open} onClick={this.open}
src={chrome.extension.getURL(logoUrl)} src={chrome.extension.getURL(logoUrl)}
width="50%"
/> />
{this.bookedHours > 0 && <span className="booked-hours"><small>{this.bookedHours}h</small></span>} {this.bookedHours > 0 && <span className="booked-hours"><small>{this.bookedHours}h</small></span>}
{this.isOpen && ( {this.isOpen && (
<Modal> <Modal>
<Content>{this.renderContent()}</Content> {this.renderContent()}
</Modal> </Modal>
)} )}
</> </div>
) )
} }
} }

View File

@@ -1,5 +1,4 @@
import React, { Component } from "react" import React, { Component } from "react"
import { createPortal } from "react-dom"
import PropTypes from "prop-types" import PropTypes from "prop-types"
class Modal extends Component { class Modal extends Component {
@@ -7,35 +6,17 @@ class Modal extends Component {
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
} }
constructor(props) {
super(props)
this.el = document.createElement("div")
this.el.setAttribute("class", "moco-bx-modal")
}
componentDidMount() {
const modalRoot = document.getElementById("moco-bx-container")
modalRoot.appendChild(this.el)
}
componentWillUnmount() {
const modalRoot = document.getElementById("moco-bx-container")
modalRoot.removeChild(this.el)
}
// RENDER ------------------------------------------------------------------- // RENDER -------------------------------------------------------------------
render() { render() {
return createPortal(this.props.children, this.el) return (
<div className="moco-bx-modal">
<div className="moco-bx-modal-content">
{this.props.children}
</div>
</div>
)
} }
} }
export function Content({ children }) {
return <div className="moco-bx-modal-content">{children}</div>
}
Content.propTypes = {
children: PropTypes.node
}
export default Modal export default Modal

View File

@@ -12,7 +12,6 @@ import {
flatMap, flatMap,
pathEq pathEq
} from "lodash/fp" } from "lodash/fp"
import { trace } from "utils"
const customTheme = theme => ({ const customTheme = theme => ({
...theme, ...theme,

View File

@@ -1,9 +1,7 @@
import React from 'react' import React from 'react'
const Spinner = () => ( const Spinner = () => (
<div className="spinner" role="status"> <div className="moco-bx-spinner" role="status" />
<span className="sr-only">Loading...</span>
</div>
) )
export default Spinner export default Spinner

View File

@@ -1,11 +1,11 @@
import { createElement } from "react" import React from "react"
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import ShadowDOM from "react-shadow"
import Bubble from "./components/Bubble" import Bubble from "./components/Bubble"
import services from "remoteServices" import { createMatcher, createEnhancer } from "utils/urlMatcher"
import { parseServices, createMatcher, createEnhancer } from "utils/urlMatcher"
import remoteServices from "./remoteServices" import remoteServices from "./remoteServices"
import { pipe } from 'lodash/fp' import { pipe } from 'lodash/fp'
import "../css/main.scss" import "../css/content.scss"
const matcher = createMatcher(remoteServices) const matcher = createMatcher(remoteServices)
const serviceEnhancer = createEnhancer(window.document) const serviceEnhancer = createEnhancer(window.document)
@@ -32,35 +32,27 @@ const mountBubble = (settings) => {
return return
} }
if (!document.getElementById("moco-bx-container")) { if (!document.getElementById("moco-bx-root")) {
const domContainer = document.createElement("div") const domRoot = document.createElement("div")
domContainer.setAttribute("id", "moco-bx-container") domRoot.setAttribute("id", "moco-bx-root")
document.body.appendChild(domContainer) document.body.appendChild(domRoot)
}
if (!document.getElementById("moco-bx-bubble")) {
const domBubble = document.createElement("div")
domBubble.setAttribute("id", "moco-bx-bubble")
document.body.appendChild(domBubble)
} }
ReactDOM.render( ReactDOM.render(
createElement(Bubble, { service, settings }), <ShadowDOM include={[chrome.extension.getURL('content.css')]}>
document.getElementById("moco-bx-bubble") <div>
<Bubble service={service} settings={settings} />
</div>
</ShadowDOM>,
document.getElementById("moco-bx-root")
) )
} }
const unmountBubble = () => { const unmountBubble = () => {
const domBubble = document.getElementById("moco-bx-bubble") const domRoot = document.getElementById("moco-bx-root")
const domContainer = document.getElementById("moco-bx-container")
if (domBubble) { if (domRoot) {
ReactDOM.unmountComponentAtNode(domBubble) ReactDOM.unmountComponentAtNode(domRoot)
domBubble.remove() domRoot.remove()
}
if (domContainer) {
ReactDOM.unmountComponentAtNode(domContainer)
domContainer.remove()
} }
} }

View File

@@ -1,7 +1,7 @@
import { createElement } from "react" import React from "react"
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import Setup from "./components/Setup" import Setup from "./components/Setup"
import "../css/main.scss" import "../css/options.scss"
const domContainer = document.querySelector("#moco-bx-container") const domContainer = document.querySelector("#moco-bx-root")
ReactDOM.render(createElement(Setup), domContainer) ReactDOM.render(<Setup />, domContainer)

View File

@@ -1,6 +0,0 @@
import { createElement } from 'react'
import ReactDOM from 'react-dom'
import Form from './components/Form'
const domContainer = document.querySelector('#moco-bx-container')
ReactDOM.render(createElement(Form, {inline: false}), domContainer)

View File

@@ -25,6 +25,12 @@ export default {
description: (document, service, { org, repo, id }) => description: (document, service, { org, repo, id }) =>
`${org}/${repo}/${id} - ${document `${org}/${repo}/${id} - ${document
.querySelector(".gh-header-title") .querySelector(".gh-header-title")
.textContent.trim()}` .textContent.trim()}`,
},
"trello": {
name: "trello",
urlPattern: "https://trello.com/c/:id/:title",
description: (document, service, { title }) => title.split('-').slice(1).join(' ')
} }
} }

View File

@@ -28,7 +28,8 @@
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": ["<all_urls>"],
"js": ["content.js"] "js": ["content.js"],
"css": ["styles.css"]
} }
], ],
"browser_action": { "browser_action": {
@@ -36,7 +37,7 @@
"default_title": "MOCO Time Tracking", "default_title": "MOCO Time Tracking",
"default_popup": "popup.html" "default_popup": "popup.html"
}, },
"web_accessible_resources": ["src/images/*"], "web_accessible_resources": ["src/images/*", "content.css"],
"commands": { "commands": {
"_execute_browser_action": { "_execute_browser_action": {
"suggested_key": { "suggested_key": {

View File

@@ -4,6 +4,6 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
</head> </head>
<body> <body>
<div id="moco-bx-container"></div> <div id="moco-bx-root"></div>
</body> </body>
</html> </html>

View File

@@ -1,9 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="moco-bx-container"></div>
</body>
</html>

View File

@@ -1,4 +1,5 @@
const path = require("path") const path = require("path")
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const CleanWebpackPlugin = require("clean-webpack-plugin") const CleanWebpackPlugin = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin") const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin") const CopyWebpackPlugin = require("copy-webpack-plugin")
@@ -7,8 +8,7 @@ module.exports = {
entry: { entry: {
background: "./src/js/background.js", background: "./src/js/background.js",
content: "./src/js/content.js", content: "./src/js/content.js",
options: "./src/js/options.js", options: "./src/js/options.js"
popup: "./src/js/popup.js"
}, },
output: { output: {
path: path.join(__dirname, "build"), path: path.join(__dirname, "build"),
@@ -19,7 +19,9 @@ module.exports = {
{ {
test: /\.scss$/, test: /\.scss$/,
use: [ use: [
"style-loader", {
loader: MiniCssExtractPlugin.loader
},
"css-loader", "css-loader",
{ {
loader: "sass-loader", loader: "sass-loader",
@@ -48,6 +50,10 @@ module.exports = {
] ]
}, },
plugins: [ plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
chunkFilename: '[id].css'
}),
new CleanWebpackPlugin(["build"]), new CleanWebpackPlugin(["build"]),
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{ {
@@ -62,13 +68,11 @@ module.exports = {
}) })
) )
} }
},
{
from: "src/css/styles.css",
} }
]), ]),
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "popup.html"),
filename: "popup.html",
chunks: ["popup"]
}),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "options.html"), template: path.join(__dirname, "src", "options.html"),
filename: "options.html", filename: "options.html",

View File

@@ -4563,6 +4563,15 @@ mimic-fn@^1.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
mini-css-extract-plugin@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==
dependencies:
loader-utils "^1.1.0"
schema-utils "^1.0.0"
webpack-sources "^1.1.0"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@@ -5642,6 +5651,11 @@ react-select@^2.3.0:
react-input-autosize "^2.2.1" react-input-autosize "^2.2.1"
react-transition-group "^2.2.1" react-transition-group "^2.2.1"
react-shadow@^16.3.2:
version "16.3.2"
resolved "https://registry.yarnpkg.com/react-shadow/-/react-shadow-16.3.2.tgz#a431a7101b0e222dbc669c4070ab395f42dbb675"
integrity sha512-NxFoBvEg2WD3V25jgiWgIltpDfs2XBCbDyi63jEM6Gch8g8r2cqJxSWy2GfG666EfOpRR5Zsrhu0Qn5yze1PrA==
react-transition-group@^2.2.1: react-transition-group@^2.2.1:
version "2.5.3" version "2.5.3"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.3.tgz#26de363cab19e5c88ae5dbae105c706cf953bb92" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.3.tgz#26de363cab19e5c88ae5dbae105c706cf953bb92"