From 83b7182cd54580925fddebf34ffe1f12c9fe8195 Mon Sep 17 00:00:00 2001 From: rugk Date: Sat, 15 Dec 2018 22:49:44 +0100 Subject: [PATCH] Integrate TinyWebEx modules/templates --- .gitmodules | 12 + CONTRIBUTORS | 5 + src/_locales/de/messages.json | 61 ++++- src/_locales/en/messages.json | 59 ++++ src/_locales/tr/messages.json | 57 ++++ src/common/common.css | 257 ++++++++++++++++++ src/common/common.js | 9 + src/common/img/check.svg | 6 + src/common/img/close-white.svg | 6 + src/common/img/close.svg | 6 + src/common/img/error-white.svg | 6 + src/common/img/info-dark.svg | 6 + src/common/img/info-light.svg | 6 + src/common/img/open-in-new.svg | 7 + src/common/img/warning-dark.svg | 6 + src/common/modules/AddonSettings | 1 + src/common/modules/AutomaticSettings | 1 + src/common/modules/Localizer | 1 + src/common/modules/MessageHandler | 1 + src/common/modules/data/DefaultSettings.js | 9 + src/common/modules/data/MessageLevel.js | 20 ++ .../modules/lodash/.internal/baseGetTag.js | 39 +++ .../modules/lodash/.internal/freeGlobal.js | 4 + src/common/modules/lodash/.internal/getTag.js | 51 ++++ src/common/modules/lodash/.internal/root.js | 9 + src/common/modules/lodash/debounce.js | 212 +++++++++++++++ src/common/modules/lodash/isFunction.js | 30 ++ src/common/modules/lodash/isObject.js | 29 ++ src/common/modules/lodash/isObjectLike.js | 27 ++ src/common/modules/lodash/isPlainObject.js | 44 +++ src/common/modules/lodash/isString.js | 23 ++ src/common/modules/lodash/throttle.js | 70 +++++ src/common/variables.css | 58 ++++ src/options/fastLoad.js | 4 + src/options/options.css | 41 ++- src/options/options.html | 38 ++- src/options/options.js | 25 +- 37 files changed, 1217 insertions(+), 29 deletions(-) create mode 100644 .gitmodules create mode 100644 src/_locales/tr/messages.json create mode 100644 src/common/common.css create mode 100644 src/common/common.js create mode 100644 src/common/img/check.svg create mode 100644 src/common/img/close-white.svg create mode 100644 src/common/img/close.svg create mode 100644 src/common/img/error-white.svg create mode 100644 src/common/img/info-dark.svg create mode 100644 src/common/img/info-light.svg create mode 100644 src/common/img/open-in-new.svg create mode 100644 src/common/img/warning-dark.svg create mode 160000 src/common/modules/AddonSettings create mode 160000 src/common/modules/AutomaticSettings create mode 160000 src/common/modules/Localizer create mode 160000 src/common/modules/MessageHandler create mode 100644 src/common/modules/data/DefaultSettings.js create mode 100644 src/common/modules/data/MessageLevel.js create mode 100644 src/common/modules/lodash/.internal/baseGetTag.js create mode 100644 src/common/modules/lodash/.internal/freeGlobal.js create mode 100644 src/common/modules/lodash/.internal/getTag.js create mode 100644 src/common/modules/lodash/.internal/root.js create mode 100644 src/common/modules/lodash/debounce.js create mode 100644 src/common/modules/lodash/isFunction.js create mode 100644 src/common/modules/lodash/isObject.js create mode 100644 src/common/modules/lodash/isObjectLike.js create mode 100644 src/common/modules/lodash/isPlainObject.js create mode 100644 src/common/modules/lodash/isString.js create mode 100644 src/common/modules/lodash/throttle.js create mode 100644 src/common/variables.css create mode 100644 src/options/fastLoad.js diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..31387bd --- /dev/null +++ b/.gitmodules @@ -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 diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e69de29..c516edb 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -0,0 +1,5 @@ +@ENT8R + +translations: +German: @rugk +Turkish: Ömür Turan (@omurturan) diff --git a/src/_locales/de/messages.json b/src/_locales/de/messages.json index b1b1603..9d42c3b 100644 --- a/src/_locales/de/messages.json +++ b/src/_locales/de/messages.json @@ -9,7 +9,66 @@ "description": "Name of the extension." }, "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." + }, + + // 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" } } diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index c1c634a..1441cc2 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -11,5 +11,64 @@ "extensionDescription": { "message": "Simplifies following or interacting with other users on remote instances.", "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" } } diff --git a/src/_locales/tr/messages.json b/src/_locales/tr/messages.json new file mode 100644 index 0000000..27815a2 --- /dev/null +++ b/src/_locales/tr/messages.json @@ -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" + } +} diff --git a/src/common/common.css b/src/common/common.css new file mode 100644 index 0000000..f1fe89c --- /dev/null +++ b/src/common/common.css @@ -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; +} diff --git a/src/common/common.js b/src/common/common.js new file mode 100644 index 0000000..ee09b3b --- /dev/null +++ b/src/common/common.js @@ -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"; diff --git a/src/common/img/check.svg b/src/common/img/check.svg new file mode 100644 index 0000000..b2aebe1 --- /dev/null +++ b/src/common/img/check.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/close-white.svg b/src/common/img/close-white.svg new file mode 100644 index 0000000..8839154 --- /dev/null +++ b/src/common/img/close-white.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/close.svg b/src/common/img/close.svg new file mode 100644 index 0000000..771ddb4 --- /dev/null +++ b/src/common/img/close.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/error-white.svg b/src/common/img/error-white.svg new file mode 100644 index 0000000..0bad8b0 --- /dev/null +++ b/src/common/img/error-white.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/info-dark.svg b/src/common/img/info-dark.svg new file mode 100644 index 0000000..f3dbfd7 --- /dev/null +++ b/src/common/img/info-dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/info-light.svg b/src/common/img/info-light.svg new file mode 100644 index 0000000..c75d26b --- /dev/null +++ b/src/common/img/info-light.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/img/open-in-new.svg b/src/common/img/open-in-new.svg new file mode 100644 index 0000000..53e39e4 --- /dev/null +++ b/src/common/img/open-in-new.svg @@ -0,0 +1,7 @@ + + + + + diff --git a/src/common/img/warning-dark.svg b/src/common/img/warning-dark.svg new file mode 100644 index 0000000..e6fa6e5 --- /dev/null +++ b/src/common/img/warning-dark.svg @@ -0,0 +1,6 @@ + + + + diff --git a/src/common/modules/AddonSettings b/src/common/modules/AddonSettings new file mode 160000 index 0000000..57bb908 --- /dev/null +++ b/src/common/modules/AddonSettings @@ -0,0 +1 @@ +Subproject commit 57bb908833485bb34cd03f9b4976e4aeee4f4463 diff --git a/src/common/modules/AutomaticSettings b/src/common/modules/AutomaticSettings new file mode 160000 index 0000000..73534db --- /dev/null +++ b/src/common/modules/AutomaticSettings @@ -0,0 +1 @@ +Subproject commit 73534db51b389a4e20cab3ed6801fd02cc405c0b diff --git a/src/common/modules/Localizer b/src/common/modules/Localizer new file mode 160000 index 0000000..1aca1fb --- /dev/null +++ b/src/common/modules/Localizer @@ -0,0 +1 @@ +Subproject commit 1aca1fb9b5dd7f872002e1fe168b9926c80c6f9c diff --git a/src/common/modules/MessageHandler b/src/common/modules/MessageHandler new file mode 160000 index 0000000..9dc7295 --- /dev/null +++ b/src/common/modules/MessageHandler @@ -0,0 +1 @@ +Subproject commit 9dc729591ea410f1c3108df8afadba5f26050fb0 diff --git a/src/common/modules/data/DefaultSettings.js b/src/common/modules/data/DefaultSettings.js new file mode 100644 index 0000000..2a204c7 --- /dev/null +++ b/src/common/modules/data/DefaultSettings.js @@ -0,0 +1,9 @@ +/** + * Specifies the default settings of the add-on. + * + * @module data/DefaultSettings + */ + +export const DEFAULT_SETTINGS = Object.freeze({ + debugMode: false, +}); diff --git a/src/common/modules/data/MessageLevel.js b/src/common/modules/data/MessageLevel.js new file mode 100644 index 0000000..415ee40 --- /dev/null +++ b/src/common/modules/data/MessageLevel.js @@ -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 +}); diff --git a/src/common/modules/lodash/.internal/baseGetTag.js b/src/common/modules/lodash/.internal/baseGetTag.js new file mode 100644 index 0000000..870c63f --- /dev/null +++ b/src/common/modules/lodash/.internal/baseGetTag.js @@ -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 diff --git a/src/common/modules/lodash/.internal/freeGlobal.js b/src/common/modules/lodash/.internal/freeGlobal.js new file mode 100644 index 0000000..7b0963c --- /dev/null +++ b/src/common/modules/lodash/.internal/freeGlobal.js @@ -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 diff --git a/src/common/modules/lodash/.internal/getTag.js b/src/common/modules/lodash/.internal/getTag.js new file mode 100644 index 0000000..d43709d --- /dev/null +++ b/src/common/modules/lodash/.internal/getTag.js @@ -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 diff --git a/src/common/modules/lodash/.internal/root.js b/src/common/modules/lodash/.internal/root.js new file mode 100644 index 0000000..8a4324d --- /dev/null +++ b/src/common/modules/lodash/.internal/root.js @@ -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 diff --git a/src/common/modules/lodash/debounce.js b/src/common/modules/lodash/debounce.js new file mode 100644 index 0000000..581a4c2 --- /dev/null +++ b/src/common/modules/lodash/debounce.js @@ -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 diff --git a/src/common/modules/lodash/isFunction.js b/src/common/modules/lodash/isFunction.js new file mode 100644 index 0000000..7f593cb --- /dev/null +++ b/src/common/modules/lodash/isFunction.js @@ -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 diff --git a/src/common/modules/lodash/isObject.js b/src/common/modules/lodash/isObject.js new file mode 100644 index 0000000..5ad8eaf --- /dev/null +++ b/src/common/modules/lodash/isObject.js @@ -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 diff --git a/src/common/modules/lodash/isObjectLike.js b/src/common/modules/lodash/isObjectLike.js new file mode 100644 index 0000000..2e575de --- /dev/null +++ b/src/common/modules/lodash/isObjectLike.js @@ -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 diff --git a/src/common/modules/lodash/isPlainObject.js b/src/common/modules/lodash/isPlainObject.js new file mode 100644 index 0000000..34dcf9a --- /dev/null +++ b/src/common/modules/lodash/isPlainObject.js @@ -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 diff --git a/src/common/modules/lodash/isString.js b/src/common/modules/lodash/isString.js new file mode 100644 index 0000000..b9ec3ae --- /dev/null +++ b/src/common/modules/lodash/isString.js @@ -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 diff --git a/src/common/modules/lodash/throttle.js b/src/common/modules/lodash/throttle.js new file mode 100644 index 0000000..2dbfdf0 --- /dev/null +++ b/src/common/modules/lodash/throttle.js @@ -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 diff --git a/src/common/variables.css b/src/common/variables.css new file mode 100644 index 0000000..2bcc494 --- /dev/null +++ b/src/common/variables.css @@ -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); +} diff --git a/src/options/fastLoad.js b/src/options/fastLoad.js new file mode 100644 index 0000000..b4150bd --- /dev/null +++ b/src/options/fastLoad.js @@ -0,0 +1,4 @@ +import * as MobileOptions from "/common/modules/AutomaticSettings/MobileOptions.js"; + +// hide not mobile compatible settings +MobileOptions.init(); diff --git a/src/options/options.css b/src/options/options.css index 7854124..0cce3dd 100644 --- a/src/options/options.css +++ b/src/options/options.css @@ -1,20 +1,18 @@ body { /* 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; } /* on (small) mobile displays */ -@media (max-width: 700px) { - body { - /* smaller size -> default on Firefox for Android is 14px */ - font-size: 14px; - } +body.mobile { + /* smaller size -> default on Firefox for Android is 14px */ + font-size: 14px; +} - /* e.g. the icon is not even displayed there :) */ - .mobile-incompatible { - display: none; - } +/* disable all options incomaptible with mobile devices */ +body.mobile .mobile-incompatible { + display: none; } /* https://design.firefox.com/photon/patterns/inactive.html */ @@ -28,7 +26,7 @@ ul { } li { list-style-type: none; - margin-top: 8px; + margin-top: 10px; padding: 0px; } /* in a fieldset e.g. we use a condensed version */ @@ -54,3 +52,24 @@ fieldset { label + .setting { 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; +} + diff --git a/src/options/options.html b/src/options/options.html index be522a1..2612329 100644 --- a/src/options/options.html +++ b/src/options/options.html @@ -3,20 +3,54 @@ + + + + +
+ + + + +
diff --git a/src/options/options.js b/src/options/options.js index 9d6bb63..877a666 100644 --- a/src/options/options.js +++ b/src/options/options.js @@ -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", () => { - const ownMastodonSplit = Mastodon.splitUserHandle(insertHandle.value); - - 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; -}); +// init modules +AutomaticSettings.setDefaultOptionProvider(AddonSettings.getDefaultValue); +AutomaticSettings.init();