Integrate TinyWebEx modules/templates

This commit is contained in:
rugk 2018-12-15 22:49:44 +01:00
parent fffa1085e0
commit 83b7182cd5
No known key found for this signature in database
GPG key ID: 05D40A636AFAB34D
37 changed files with 1217 additions and 29 deletions

12
.gitmodules vendored Normal file
View file

@ -0,0 +1,12 @@
[submodule "src/common/modules/AutomaticSettings"]
path = src/common/modules/AutomaticSettings
url = https://github.com/TinyWebEx/AutomaticSettings
[submodule "src/common/modules/MessageHandler"]
path = src/common/modules/MessageHandler
url = https://github.com/TinyWebEx/MessageHandler
[submodule "src/common/modules/Localizer"]
path = src/common/modules/Localizer
url = https://github.com/TinyWebEx/Localizer
[submodule "src/common/modules/AddonSettings"]
path = src/common/modules/AddonSettings
url = https://github.com/TinyWebEx/AddonSettings

View file

@ -0,0 +1,5 @@
@ENT8R
translations:
German: @rugk
Turkish: Ömür Turan (@omurturan)

View file

@ -9,7 +9,66 @@
"description": "Name of the extension." "description": "Name of the extension."
}, },
"extensionDescription": { "extensionDescription": {
"message": "Vereinfacht das Folgen und das Interagieren mit Nutzern auf anderen Instanzen.", "message": "Vereinfacht das Folgen und Interagieren mit Nutzern auf anderen Instanzen.",
"description": "Description of the extension." "description": "Description of the extension."
},
// errors or other messages (mostly for settings)
"errorShowingMessage": {
"message": "Konnte Nachricht nicht anzeigen.",
"description": "When there is an error when showing the error/info/…."
},
"couldNotLoadOptions": {
"message": "Konnte Einstellungen nicht laden.",
"description": "When one or all settings could not be loaded."
},
"couldNotSaveOption": {
"message": "Konnte diese Einstellung nicht speichern.",
"description": "When a setting could not be saved."
},
"messageUndoButton": {
"message": "Rückgängig machen",
"description": "The text of a button that undoes the last action."
},
"couldNotUndoAction": {
"message": "Konnte Aktion nicht rückgängig machen.",
"description": "Shown when an action cannot be undone."
},
"resettingOptionsWorked": {
"message": "Alle Einstellungen benutzen nun wieder die Standardwerte!",
"description": "The message shown, when the options of the settings were reset."
},
"resettingOptionsFailed": {
"message": "Konnte Optionen nicht zurück setzen!",
"description": "The message shown, when the options of the settings could not have been reset."
},
// options
"someSettingsAreManaged": {
"message": "Einige Einstellung werden von deinem Systemadministrator festgelegt und können nicht geändert werden.",
"description": "The message, which appears, when settings are pre-defined (as managed options) by administrators."
},
"optionIsDisabledBecauseManaged": {
"message": "Diese option ist deaktiviert, weil sie von deinem Systemadministrator festgelegt wurde.",
"description": "The title (tooltip) shown, when hovering over a disabled, managed option."
},
"optionLearnMore": {
"message": "Weitere Informationen",
"description": "When a link to an explainer needs to be added, this is the link text."
},
"optionsResetButton": {
"message": "Setze alle Einstellungen auf Standardwerte zurück",
"description": "The button to delete all current settings and load the defaults, shown in the add-on settings."
},
"optionMastodonHandle": {
"message": "Dein Mastodon-Handle: ",
"description": "This is an option shown in the add-on settings. You can enter your handle in the form mastodon@server.com there."
},
// ARIA labels/descriptions
"dismissIconDescription": {
"message": "Diese Nachricht schließen",
"description": "the label for the close button of the message box"
} }
} }

View file

@ -11,5 +11,64 @@
"extensionDescription": { "extensionDescription": {
"message": "Simplifies following or interacting with other users on remote instances.", "message": "Simplifies following or interacting with other users on remote instances.",
"description": "Description of the extension." "description": "Description of the extension."
},
// errors or other messages (mostly for settings)
"errorShowingMessage": {
"message": "Could not show this message.",
"description": "When there is an error when showing the error/info/…."
},
"couldNotLoadOptions": {
"message": "Could not load settings.",
"description": "When one or all settings could not be loaded."
},
"couldNotSaveOption": {
"message": "Could not save this setting.",
"description": "When a setting could not be saved."
},
"messageUndoButton": {
"message": "Undo",
"description": "The text of a button that undoes the last action."
},
"couldNotUndoAction": {
"message": "Could not undo action.",
"description": "Shown when an action cannot be undone."
},
"resettingOptionsWorked": {
"message": "All settings are now back to defaults again!",
"description": "The message shown, when the options of the settings were reset."
},
"resettingOptionsFailed": {
"message": "Could not reset options!",
"description": "The message shown, when the options of the settings could not have been reset."
},
// options
"someSettingsAreManaged": {
"message": "Some settings are managed by your system administrator and cannot be changed.",
"description": "The message, which appears, when settings are pre-defined (as managed options) by administrators."
},
"optionIsDisabledBecauseManaged": {
"message": "This option is disabled, because it has been configured by your system administrator.",
"description": "The title (tooltip) shown, when hovering over a disabled, managed option."
},
"optionLearnMore": {
"message": "Learn more",
"description": "When a link to an explainer needs to be added, this is the link text."
},
"optionsResetButton": {
"message": "Reset all settings to defaults",
"description": "The button to delete all current settings and load the defaults, shown in the add-on settings."
},
"optionMastodonHandle": {
"message": "Your Mastodon handle: ",
"description": "This is an option shown in the add-on settings. You can enter your handle in the form mastodon@server.com there."
},
// ARIA labels/descriptions
"dismissIconDescription": {
"message": "Close this message",
"description": "the label for the close button of the message box"
} }
} }

View file

@ -0,0 +1,57 @@
{
// manifest.json
// errors or other messages (mostly for settings)
"errorShowingMessage": {
"message": "Mesaj görüntülenemedi.",
"description": "When there is an error when showing the error/info/…."
},
"couldNotLoadOptions": {
"message": "Ayarlar yüklenemedi.",
"description": "When one or all settings could not be loaded."
},
"couldNotSaveOption": {
"message": "Ayarlar kaydedilemedi.",
"description": "When a setting could not be saved."
},
"messageUndoButton": {
"message": "Geri al",
"description": "The text of a button that undoes the last action."
},
"couldNotUndoAction": {
"message": "Geri alma işlemi başarısız.",
"description": "Shown when an action cannot be undone."
},
"resettingOptionsWorked": {
"message": "Bütün ayarlar varsayılan değerlere döndürüldü!",
"description": "The message shown, when the options of the settings were reset."
},
"resettingOptionsFailed": {
"message": "Seçenekleri sıfırlama başarısız oldu!",
"description": "The message shown, when the options of the settings could not have been reset."
},
// options
"someSettingsAreManaged": {
"message": "Bazı ayarlar sistem yöneticin tarafından düzenlendiği için değiştirilemez.",
"description": "The message, which appears, when settings are pre-defined (as managed options) by administrators."
},
"optionIsDisabledBecauseManaged": {
"message": "Bu özellik sistem yöneticin tarafından ayarlandığı için devredışı.",
"description": "The title (tooltip) shown, when hovering over a disabled, managed option."
},
"optionLearnMore": {
"message": "Daha fazla bilgi",
"description": "When a link to an explainer needs to be added, this is the link text."
},
"optionsResetButton": {
"message": "Bütün ayarları varsayılanlara sıfırla",
"description": "The button to delete all current settings and load the defaults, shown in the add-on settings."
},
// ARIA labels/descriptions
"dismissIconDescription": {
"message": "Bu mesajı kapat",
"description": "the label for the close button of the message box"
}
}

257
src/common/common.css Normal file
View file

@ -0,0 +1,257 @@
@import url("./variables.css");
body {
direction: __MSG_@@bidi_dir__;
}
/* https://design.firefox.com/photon/components/links.html */
a {
color: var(--blue-60);
text-decoration: none;
}
a:focus {
border-radius: 4px;
box-shadow: 0 0 0 2px var(--blue-50), 0 0 0 6px var(--blue-50-a30);
}
a:hover, a:active {
text-decoration: underline;
}
a:active {
color: var(--blue-70);
}
/* external link symbol */
/* currently disabled, because it is not clear what an external link is, in our case */
/*a:not([class])[href*="//"]::after {
background-image: url(/common/img/open-in-new.svg);
background-repeat: no-repeat;
background-size: 16px 16px;
content: "";
display: inline-block;
height: 16px;
margin: -.3rem .15rem 0 .25rem;
vertical-align: middle;
width: 16px;
}*/
/* small classes in order to avoid inline CSS */
.invisible {
display: none !important;
}
.message-container {
position: relative;
}
/* buttons https://design.firefox.com/photon/components/buttons.html */
.micro-button {
min-height: 24px;
height: auto;
border-radius: 2px;
padding-left: 8px;
padding-right: 8px;
/* not documented, but looks ugly otherwise */
padding-top: 2px;
padding-bottom: 2px;
box-sizing: content-box;
/* do not break over multiple lines */
/* white-space: nowrap; */
height: auto; /* currently, we rather prefer breaking until https://github.com/rugk/offline-qr-code/issues/12 is done */
}
/* use light color for dark backgrounds */
.micro-button:hover.success,
.micro-button:active.success,
.micro-button:hover.warning,
.micro-button:active.warning,
.micro-button:hover.error,
.micro-button:active.error {
color: var(--white-100);
}
.micro-button.info {
background-color: var(--grey-90-a10);
}
.micro-button:hover.info {
background-color: var(--grey-90-a20);
}
.micro-button:active.info {
background-color: var(--grey-90-a30);
}
.micro-button.success {
background-color: var(--green-60);
}
.micro-button:hover.success {
background-color: var(--green-70);
}
.micro-button:active.success {
background-color: var(--green-80);
}
.micro-button.warning {
background-color: var(--yellow-60);
}
.micro-button:hover.warning {
background-color: var(--yellow-70);
}
.micro-button:active.warning {
background-color: var(--yellow-80);
}
.micro-button.error {
background-color: var(--red-70);
color: var(--white-100);
}
.micro-button:hover.error {
background-color: var(--red-80);
}
.micro-button:active.error {
background-color: var(--red-90);
}
.micro-button:focus {
box-shadow: 0 0 0 1px #0a84ff inset, 0 0 0 1px #0a84ff, 0 0 0 4px rgba(10, 132, 255, 0.3)
}
/* message box */
/* follows https://design.firefox.com/photon/components/message-bars.html */
.message-box {
padding: 4px;
border-radius: 4px;
/* use whole width */
width: 100%;
min-height: 32px;
/* make errors selectable, so users can copy them */
-moz-user-select: text;
cursor: text;
/* multiline */
hypens: auto;
overflow-wrap: break-word;
/* center-vertically */
display: flex;
align-items: center;
z-index: 2;
/* fade-in transition */
/* follow https://design.firefox.com/photon/motion/duration-and-easing.html */
opacity: 1;
max-height: 100px;
transition: opacity 150ms cubic-bezier(.07,.95,0,1),
max-height 200ms cubic-bezier(.07,.95,0,1);
}
.message-box.fade-hide {
max-height: 0px;
opacity: 0;
min-height: 0px;
}
/* add margin when messages are stacked on each other */
.message-box:not(.invisible) ~ .message-box:not(.invisible) {
margin-top: 8px;
}
.error {
color: var(--white-100);
background-color: var(--red-60);
}
.info {
color: var(--grey-90);
background-color: var(--grey-20);
}
.success {
color: var(--green-90);
background-color: var(--green-50);
}
.warning {
color: var(--yellow-90);
background-color: var(--yellow-50);
}
/* message box action button */
.message-action-button {
margin-left: 8px;
/* center vertially */
margin-top: auto;
margin-bottom: auto;
/* some minimum margin to dismiss button or similar */
margin-right: 4px;
border: 0;
color: var(--grey-90);
cursor: pointer;
}
/* icons for the message boxes */
.message-box::before {
display: inline-block;
/* fixed size */
background-size: 16px 16px;
width: 16px;
height: 16px;
min-width: 16px;
min-height: 16px;
content: "";
margin: 4px;
}
.error::before {
background-image: url('/common/img/error-white.svg');
}
.info::before {
background-image: url('/common/img/info-dark.svg');
}
.success::before {
background-image: url('/common/img/check.svg');
}
.warning::before {
background-image: url('/common/img/warning-dark.svg');
}
.icon-dismiss {
box-sizing: content-box;
padding: 2px;
width: 24px;
height: 24px;
margin-left: auto;
cursor: pointer;
/* some animation on hover */
transition: background-color 150ms cubic-bezier(.07,.95,0,1);
}
.icon-dismiss:hover {
background-color: var(--grey-90-a10);
border-radius: 2px;
}
.icon-dismiss:active {
background-color: var(--grey-90-a20);
border-radius: 2px;
}
.icon-dismiss:focus {
box-shadow: 0 0 0 1px var(--blue-50) inset, 0 0 0 1px var(--blue-50), 0 0 0 4px var(--blue-50-a30);
border-radius: 2px;
}

9
src/common/common.js Normal file
View file

@ -0,0 +1,9 @@
/**
* Just load/does common stuff.
*
* @module common
* @requires modules/Localizer
*/
// by default translate whole site
import "/common/modules/Localizer/Localizer.js";

6
src/common/img/check.svg Normal file
View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M6 14a1 1 0 0 1-.707-.293l-3-3a1 1 0 0 1 1.414-1.414l2.157 2.157 6.316-9.023a1 1 0 0 1 1.639 1.146l-7 10a1 1 0 0 1-.732.427A.863.863 0 0 1 6 14z"></path>
</svg>

After

Width:  |  Height:  |  Size: 488 B

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="rgba(249, 249, 250, .8)" d="M9.061 8l3.47-3.47a.75.75 0 0 0-1.061-1.06L8 6.939 4.53 3.47a.75.75 0 1 0-1.06 1.06L6.939 8 3.47 11.47a.75.75 0 1 0 1.06 1.06L8 9.061l3.47 3.47a.75.75 0 0 0 1.06-1.061z"></path>
</svg>

After

Width:  |  Height:  |  Size: 523 B

6
src/common/img/close.svg Normal file
View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="context-fill" d="M9.061 8l3.47-3.47a.75.75 0 0 0-1.061-1.06L8 6.939 4.53 3.47a.75.75 0 1 0-1.06 1.06L6.939 8 3.47 11.47a.75.75 0 1 0 1.06 1.06L8 9.061l3.47 3.47a.75.75 0 0 0 1.06-1.061z"></path>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 0a8 8 0 1 0 8 8 8 8 0 0 0-8-8zM7 5a1 1 0 0 1 2 0v3a1 1 0 0 1-2 0zm1 7.19A1.19 1.19 0 1 1 9.19 11 1.19 1.19 0 0 1 8 12.19z" fill="#fff"></path>
</svg>

After

Width:  |  Height:  |  Size: 437 B

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#333" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z"></path>
</svg>

After

Width:  |  Height:  |  Size: 531 B

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="rgba(249, 249, 250, .8)" d="M8 1a7 7 0 1 0 7 7 7.008 7.008 0 0 0-7-7zm0 13a6 6 0 1 1 6-6 6.007 6.007 0 0 1-6 6zm0-7a1 1 0 0 0-1 1v3a1 1 0 1 0 2 0V8a1 1 0 0 0-1-1zm0-3.188A1.188 1.188 0 1 0 9.188 5 1.188 1.188 0 0 0 8 3.812z"></path>
</svg>

After

Width:  |  Height:  |  Size: 550 B

View file

@ -0,0 +1,7 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#0060df" d="M5 1H4a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3h8a3 3 0 0 0 3-3v-1a1 1 0 0 0-2 0v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h1a1 1 0 1 0 0-2z"></path>
<path fill="#0060df" d="M14.935 1.618A1 1 0 0 0 14.012 1h-5a1 1 0 1 0 0 2h2.586L8.305 6.293A1 1 0 1 0 9.72 7.707l3.293-3.293V7a1 1 0 1 0 2 0V2a1 1 0 0 0-.077-.382z"></path>
</svg>

After

Width:  |  Height:  |  Size: 648 B

View file

@ -0,0 +1,6 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#333" d="M14.742 12.106L9.789 2.2a2 2 0 0 0-3.578 0l-4.953 9.91A2 2 0 0 0 3.047 15h9.905a2 2 0 0 0 1.79-2.894zM7 5a1 1 0 0 1 2 0v4a1 1 0 0 1-2 0zm1 8.25A1.25 1.25 0 1 1 9.25 12 1.25 1.25 0 0 1 8 13.25z"></path>
</svg>

After

Width:  |  Height:  |  Size: 528 B

@ -0,0 +1 @@
Subproject commit 57bb908833485bb34cd03f9b4976e4aeee4f4463

@ -0,0 +1 @@
Subproject commit 73534db51b389a4e20cab3ed6801fd02cc405c0b

@ -0,0 +1 @@
Subproject commit 1aca1fb9b5dd7f872002e1fe168b9926c80c6f9c

@ -0,0 +1 @@
Subproject commit 9dc729591ea410f1c3108df8afadba5f26050fb0

View file

@ -0,0 +1,9 @@
/**
* Specifies the default settings of the add-on.
*
* @module data/DefaultSettings
*/
export const DEFAULT_SETTINGS = Object.freeze({
debugMode: false,
});

View file

@ -0,0 +1,20 @@
/**
* Contains a static object for messages.
*
* @module /common/modules/data/MessageLevel
*/
/**
* Specifies the message level to use,
*
* @readonly
* @enum {int}
* @default
*/
export const MESSAGE_LEVEL = Object.freeze({
"ERROR": 3,
"WARN": 2,
"INFO": 1,
"LOADING": -2,
"SUCCESS": -3
});

View file

@ -0,0 +1,39 @@
const objectProto = Object.prototype
const hasOwnProperty = objectProto.hasOwnProperty
const toString = objectProto.toString
const symToStringTag = typeof Symbol != 'undefined' ? Symbol.toStringTag : undefined
/**
* The base implementation of `getTag` without fallbacks for buggy environments.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
function baseGetTag(value) {
if (value == null) {
return value === undefined ? '[object Undefined]' : '[object Null]'
}
if (!(symToStringTag && symToStringTag in Object(value))) {
return toString.call(value)
}
const isOwn = hasOwnProperty.call(value, symToStringTag)
const tag = value[symToStringTag]
let unmasked = false
try {
value[symToStringTag] = undefined
unmasked = true
} catch (e) {}
const result = toString.call(value)
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag
} else {
delete value[symToStringTag]
}
}
return result
}
export default baseGetTag

View file

@ -0,0 +1,4 @@
/** Detect free variable `global` from Node.js. */
const freeGlobal = typeof global == 'object' && global !== null && global.Object === Object && global
export default freeGlobal

View file

@ -0,0 +1,51 @@
import baseGetTag from './baseGetTag.js'
/** `Object#toString` result references. */
const dataViewTag = '[object DataView]'
const mapTag = '[object Map]'
const objectTag = '[object Object]'
const promiseTag = '[object Promise]'
const setTag = '[object Set]'
const weakMapTag = '[object WeakMap]'
/** Used to detect maps, sets, and weakmaps. */
const dataViewCtorString = `${DataView}`
const mapCtorString = `${Map}`
const promiseCtorString = `${Promise}`
const setCtorString = `${Set}`
const weakMapCtorString = `${WeakMap}`
/**
* Gets the `toStringTag` of `value`.
*
* @private
* @param {*} value The value to query.
* @returns {string} Returns the `toStringTag`.
*/
let getTag = baseGetTag
// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
(getTag(new Map) != mapTag) ||
(getTag(Promise.resolve()) != promiseTag) ||
(getTag(new Set) != setTag) ||
(getTag(new WeakMap) != weakMapTag)) {
getTag = (value) => {
const result = baseGetTag(value)
const Ctor = result == objectTag ? value.constructor : undefined
const ctorString = Ctor ? `${Ctor}` : ''
if (ctorString) {
switch (ctorString) {
case dataViewCtorString: return dataViewTag
case mapCtorString: return mapTag
case promiseCtorString: return promiseTag
case setCtorString: return setTag
case weakMapCtorString: return weakMapTag
}
}
return result
}
}
export default getTag

View file

@ -0,0 +1,9 @@
import freeGlobal from './freeGlobal.js'
/** Detect free variable `self`. */
const freeSelf = typeof self == 'object' && self !== null && self.Object === Object && self
/** Used as a reference to the global object. */
const root = freeGlobal || freeSelf || Function('return this')()
export default root

View file

@ -0,0 +1,212 @@
import isObject from './isObject.js'
import root from './.internal/root.js'
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked, or until the next browser frame is drawn. The debounced function
* comes with a `cancel` method to cancel delayed `func` invocations and a
* `flush` method to immediately invoke them. Provide `options` to indicate
* whether `func` should be invoked on the leading and/or trailing edge of the
* `wait` timeout. The `func` is invoked with the last arguments provided to the
* debounced function. Subsequent calls to the debounced function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the debounced function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until the next tick, similar to `setTimeout` with a timeout of `0`.
*
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
* invocation will be deferred until the next frame is drawn (typically about
* 16ms).
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `debounce` and `throttle`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to debounce.
* @param {number} [wait=0]
* The number of milliseconds to delay; if omitted, `requestAnimationFrame` is
* used (if available).
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=false]
* Specify invoking on the leading edge of the timeout.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* jQuery(window).on('resize', debounce(calculateLayout, 150))
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* jQuery(element).on('click', debounce(sendMail, 300, {
* 'leading': true,
* 'trailing': false
* }))
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* const debounced = debounce(batchLog, 250, { 'maxWait': 1000 })
* const source = new EventSource('/stream')
* jQuery(source).on('message', debounced)
*
* // Cancel the trailing debounced invocation.
* jQuery(window).on('popstate', debounced.cancel)
*
* // Check for pending invocations.
* const status = debounced.pending() ? "Pending..." : "Ready"
*/
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export default debounce

View file

@ -0,0 +1,30 @@
import baseGetTag from './.internal/baseGetTag.js'
import isObject from './isObject.js'
/**
* Checks if `value` is classified as a `Function` object.
*
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
* @example
*
* isFunction(_)
* // => true
*
* isFunction(/abc/)
* // => false
*/
function isFunction(value) {
if (!isObject(value)) {
return false
}
// The use of `Object#toString` avoids issues with the `typeof` operator
// in Safari 9 which returns 'object' for typed arrays and other constructors.
const tag = baseGetTag(value)
return tag == '[object Function]' || tag == '[object AsyncFunction]' ||
tag == '[object GeneratorFunction]' || tag == '[object Proxy]'
}
export default isFunction

View file

@ -0,0 +1,29 @@
/**
* Checks if `value` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
*
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
* @example
*
* isObject({})
* // => true
*
* isObject([1, 2, 3])
* // => true
*
* isObject(Function)
* // => true
*
* isObject(null)
* // => false
*/
function isObject(value) {
const type = typeof value
return value != null && (type == 'object' || type == 'function')
}
export default isObject

View file

@ -0,0 +1,27 @@
/**
* Checks if `value` is object-like. A value is object-like if it's not `null`
* and has a `typeof` result of "object".
*
* @since 4.0.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
* @example
*
* isObjectLike({})
* // => true
*
* isObjectLike([1, 2, 3])
* // => true
*
* isObjectLike(Function)
* // => false
*
* isObjectLike(null)
* // => false
*/
function isObjectLike(value) {
return typeof value == 'object' && value !== null
}
export default isObjectLike

View file

@ -0,0 +1,44 @@
import baseGetTag from './.internal/baseGetTag.js'
import isObjectLike from './isObjectLike.js'
/**
* Checks if `value` is a plain object, that is, an object created by the
* `Object` constructor or one with a `[[Prototype]]` of `null`.
*
* @since 0.8.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a plain object, else `false`.
* @example
*
* function Foo() {
* this.a = 1
* }
*
* isPlainObject(new Foo)
* // => false
*
* isPlainObject([1, 2, 3])
* // => false
*
* isPlainObject({ 'x': 0, 'y': 0 })
* // => true
*
* isPlainObject(Object.create(null))
* // => true
*/
function isPlainObject(value) {
if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') {
return false
}
if (Object.getPrototypeOf(value) === null) {
return true
}
let proto = value
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(value) === proto
}
export default isPlainObject

View file

@ -0,0 +1,23 @@
import getTag from './.internal/getTag.js'
/**
* Checks if `value` is classified as a `String` primitive or object.
*
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is a string, else `false`.
* @example
*
* isString('abc')
* // => true
*
* isString(1)
* // => false
*/
function isString(value) {
const type = typeof value
return type == 'string' || (type == 'object' && value != null && !Array.isArray(value) && getTag(value) == '[object String]')
}
export default isString

View file

@ -0,0 +1,70 @@
import debounce from './debounce.js'
import isObject from './isObject.js'
/**
* Creates a throttled function that only invokes `func` at most once per
* every `wait` milliseconds (or once per browser frame). The throttled function
* comes with a `cancel` method to cancel delayed `func` invocations and a
* `flush` method to immediately invoke them. Provide `options` to indicate
* whether `func` should be invoked on the leading and/or trailing edge of the
* `wait` timeout. The `func` is invoked with the last arguments provided to the
* throttled function. Subsequent calls to the throttled function return the
* result of the last `func` invocation.
*
* **Note:** If `leading` and `trailing` options are `true`, `func` is
* invoked on the trailing edge of the timeout only if the throttled function
* is invoked more than once during the `wait` timeout.
*
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
* until the next tick, similar to `setTimeout` with a timeout of `0`.
*
* If `wait` is omitted in an environment with `requestAnimationFrame`, `func`
* invocation will be deferred until the next frame is drawn (typically about
* 16ms).
*
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
* for details over the differences between `throttle` and `debounce`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to throttle.
* @param {number} [wait=0]
* The number of milliseconds to throttle invocations to; if omitted,
* `requestAnimationFrame` is used (if available).
* @param {Object} [options={}] The options object.
* @param {boolean} [options.leading=true]
* Specify invoking on the leading edge of the timeout.
* @param {boolean} [options.trailing=true]
* Specify invoking on the trailing edge of the timeout.
* @returns {Function} Returns the new throttled function.
* @example
*
* // Avoid excessively updating the position while scrolling.
* jQuery(window).on('scroll', throttle(updatePosition, 100))
*
* // Invoke `renewToken` when the click event is fired, but not more than once every 5 minutes.
* const throttled = throttle(renewToken, 300000, { 'trailing': false })
* jQuery(element).on('click', throttled)
*
* // Cancel the trailing throttled invocation.
* jQuery(window).on('popstate', throttled.cancel)
*/
function throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
})
}
export default throttle

58
src/common/variables.css Normal file
View file

@ -0,0 +1,58 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* Photon Colors CSS Variables v3.2.0 */
/* only a subset is included, the complete declaration can be found at
* https://firefoxux.github.io/design-tokens/photon-colors/photon-colors.css
*/
:root {
--blue-50: #0a84ff;
--blue-50-a30: rgba(10, 132, 255, 0.3);
--blue-60: #0060df;
--blue-70: #003eaa;
--green-50: #30e60b;
--green-60: #12bc00;
--green-70: #058b00;
--green-80: #006504;
--green-90: #003706;
--yellow-50: #ffe900;
--yellow-60: #d7b600;
--yellow-70: #a47f00;
--yellow-80: #715100;
--yellow-90: #3e2800;
--red-50: #ff0039;
--red-60: #d70022;
--red-70: #a4000f;
--red-80: #5a0002;
--red-90: #3e0200;
--red-60: #d70022;
--grey-20: #ededf0;
--grey-30: #d7d7db;
--grey-50: #737373;
--grey-90: #0c0c0d;
--grey-90-a10: rgba(12, 12, 13, 0.1);
--grey-90-a20: rgba(12, 12, 13, 0.2);
--grey-90-a30: rgba(12, 12, 13, 0.3);
--white-100: #ffffff;
}
/* custom variables, independent of Mozilla's Photon colors
* or only based upon them */
:root {
--red-60-a90: rgba(215, 0, 34, 0.9);
--grey-20-a90: rgba(237, 237, 240, 0.9);
/* shadows https://design.firefox.com/photon/patterns/shadows.html */
--shadow-10: 0px 1px 4px var(--grey-90-a20);
--shadow-20: 0px 1px 4px var(--grey-90-a20);
--shadow-30: 0px 1px 4px var(--grey-90-a20);
}

4
src/options/fastLoad.js Normal file
View file

@ -0,0 +1,4 @@
import * as MobileOptions from "/common/modules/AutomaticSettings/MobileOptions.js";
// hide not mobile compatible settings
MobileOptions.init();

View file

@ -1,20 +1,18 @@
body { body {
/* the style the other options in the settings pane use */ /* the style the other options in the settings pane use */
font-size: 1.25rem; /* TODO: adjust to font size on mobile */ font-size: 1.11em;
color: #333; color: #333;
} }
/* on (small) mobile displays */ /* on (small) mobile displays */
@media (max-width: 700px) { body.mobile {
body { /* smaller size -> default on Firefox for Android is 14px */
/* smaller size -> default on Firefox for Android is 14px */ font-size: 14px;
font-size: 14px; }
}
/* e.g. the icon is not even displayed there :) */ /* disable all options incomaptible with mobile devices */
.mobile-incompatible { body.mobile .mobile-incompatible {
display: none; display: none;
}
} }
/* https://design.firefox.com/photon/patterns/inactive.html */ /* https://design.firefox.com/photon/patterns/inactive.html */
@ -28,7 +26,7 @@ ul {
} }
li { li {
list-style-type: none; list-style-type: none;
margin-top: 8px; margin-top: 10px;
padding: 0px; padding: 0px;
} }
/* in a fieldset e.g. we use a condensed version */ /* in a fieldset e.g. we use a condensed version */
@ -54,3 +52,24 @@ fieldset {
label + .setting { label + .setting {
margin-left: 8px; margin-left: 8px;
} }
.helper-text {
display: block;
color: var(--grey-50);
margin-top: 4px;
}
/* some margin to align with checkbox */
input[type=checkbox] ~ .helper-text,
input[type=radio] ~ .helper-text {
/* 4px margin-left + 16px size + 3px margin-right + 5px space (#text) */
margin-left: 28px;
}
/* when a link is used in a helper text, add margin */
.helper-text > a {
margin-left: 4px;
overflow-wrap: none;
}

View file

@ -3,20 +3,54 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../common/common.css">
<link rel="stylesheet" href="options.css"> <link rel="stylesheet" href="options.css">
<script async src="./fastLoad.js" type="module"></script>
<script defer src="../common/common.js" type="module"></script>
<script defer src="./options.js" type="module"></script> <script defer src="./options.js" type="module"></script>
</head> </head>
<body> <body>
<div class="message-container">
<div id="messageInfo" aria-label="info message" class="message-box info invisible fade-hide">
<span class="message-text">Some settings are managed by your administrator and cannot be changed.</span>
<a href="#">
<button class="message-action-button micro-button info invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
<div id="messageSuccess" aria-label="success message" class="message-box success invisible fade-hide">
<span class="message-text">That worked!</span>
<a href="#">
<button class="message-action-button micro-button success invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
<div id="messageError" aria-label="error message" class="message-box error invisible fade-hide">
<span class="message-text">An error happened.</span>
<a href="#">
<button class="message-action-button micro-button error invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close-white.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
<div id="messageWarning" aria-label="warning message" class="message-box warning invisible fade-hide">
<span class="message-text">There were some difficulties.</span>
<a href="#">
<button class="message-action-button micro-button warning invisible"></button>
</a>
<img class="icon-dismiss invisible" src="/common/img/close.svg" width="24" height="24" tabindex="0" data-i18n data-i18n-aria-label="__MSG_dismissIconDescription__"></span>
</div>
</div>
<form> <form>
<ul> <ul>
<li> <li>
<label data-i18n="__MSG_optionMastodonHandle__" for="insertHandle">Enter you Mastodon handle: </label> <label data-i18n="__MSG_optionMastodonHandle__" for="insertHandle">Your Mastodon handle: </label>
<input class="setting save-on-change" type="text" id="insertHandle" name="insert-handle"> <input class="setting save-on-change" type="text" id="insertHandle" name="insert-handle">
</li> </li>
<li> <li>
<button disabled data-i18n="__MSG_optionsResetButton__" type="button" name="reset-button" id="resetButton">Reset all settings to defaults</button> <button data-i18n="__MSG_optionsResetButton__" type="button" name="reset-button" id="resetButton">Reset all settings to defaults</button>
</li> </li>
</ul> </ul>
</form> </form>

View file

@ -1,17 +1,12 @@
import * as Mastodon from "/common/modules/Mastodon.js"; /**
* Starter module for addon settings site.
*
* @requires modules/OptionHandler
*/
const insertHandle = document.getElementById("insertHandle"); import * as AddonSettings from "/common/modules/AddonSettings/AddonSettings.js";
import * as AutomaticSettings from "/common/modules/AutomaticSettings/AutomaticSettings.js";
insertHandle.addEventListener("input", () => { // init modules
const ownMastodonSplit = Mastodon.splitUserHandle(insertHandle.value); AutomaticSettings.setDefaultOptionProvider(AddonSettings.getDefaultValue);
AutomaticSettings.init();
return browser.storage.sync.set({
mastodonUsername: ownMastodonSplit.username,
mastodonServer: ownMastodonSplit.server,
});
});
browser.storage.sync.get(["mastodonUsername", "mastodonServer"]).then((handleObject) => {
const mastodonHandle = Mastodon.concatUserHandle(handleObject.mastodonUsername, handleObject.mastodonServer);
insertHandle.value = mastodonHandle;
});