Merge pull request #50 from schomery/cm-and-cache
CodeMirror 5.24 + Improve style caching performance + many other features + code refactoring
|
@ -4,7 +4,6 @@ root = true
|
|||
indent_style = space
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
|
108
.eslintrc
|
@ -1,43 +1,59 @@
|
|||
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2017
|
||||
ecmaVersion: 2015
|
||||
|
||||
env:
|
||||
browser: true
|
||||
commonjs: true
|
||||
es6: true
|
||||
webextensions: true
|
||||
|
||||
globals:
|
||||
CodeMirror: false
|
||||
runTryCatch: true
|
||||
getStyles: true
|
||||
updateIcon: true
|
||||
saveStyle: true
|
||||
invalidateCache: true
|
||||
getDatabase: true
|
||||
# messaging.js
|
||||
KEEP_CHANNEL_OPEN: false
|
||||
FIREFOX: false
|
||||
OPERA: false
|
||||
URLS: false
|
||||
BG: false
|
||||
notifyAllTabs: false
|
||||
getTab: false
|
||||
getActiveTab: false
|
||||
getActiveTabRealURL: false
|
||||
getTabRealURL: false
|
||||
openURL: false
|
||||
activateTab: false
|
||||
stringAsRegExp: false
|
||||
ignoreChromeError: false
|
||||
tryCatch: false
|
||||
tryRegExp: false
|
||||
tryJSONparse: false
|
||||
debounce: false
|
||||
deepCopy: false
|
||||
onBackgroundReady: false
|
||||
deleteStyleSafe: false
|
||||
getStylesSafe: false
|
||||
saveStyleSafe: false
|
||||
sessionStorageHash: false
|
||||
download: false
|
||||
# localization.js
|
||||
template: false
|
||||
t: false
|
||||
o: false
|
||||
tE: false
|
||||
tHTML: false
|
||||
tNodeList: false
|
||||
tDocLoader: false
|
||||
# dom.js
|
||||
onDOMready: false
|
||||
scrollElementIntoView: false
|
||||
enforceInputRange: false
|
||||
animateElement: false
|
||||
$: false
|
||||
$$: false
|
||||
$element: false
|
||||
# prefs.js
|
||||
prefs: false
|
||||
reportError: true
|
||||
getActiveTab: true
|
||||
t: true
|
||||
getCodeMirrorThemes: true
|
||||
setupLivePrefs: true
|
||||
sessionStorageHash: true
|
||||
template: true
|
||||
tE: true
|
||||
tHTML: true
|
||||
CSSLint: true
|
||||
enableStyle: true
|
||||
deleteStyle: true
|
||||
getType: true
|
||||
importStyles: true
|
||||
getActiveTabRealURL: true
|
||||
getDomains: true
|
||||
webSqlStorage: true
|
||||
notifyAllTabs: true
|
||||
handleUpdate: true
|
||||
handleDelete: true
|
||||
setupLivePrefs: false
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
|
@ -47,7 +63,7 @@ rules:
|
|||
arrow-parens: [2, as-needed]
|
||||
arrow-spacing: [2, {before: true, after: true}]
|
||||
block-scoped-var: [2]
|
||||
brace-style: [2, 1tbs, {allowSingleLine: true}]
|
||||
brace-style: [2, 1tbs, {allowSingleLine: false}]
|
||||
camelcase: [2, {properties: never}]
|
||||
class-methods-use-this: [2]
|
||||
comma-dangle: [0]
|
||||
|
@ -68,16 +84,15 @@ rules:
|
|||
func-names: [0]
|
||||
generator-star-spacing: [2, before]
|
||||
global-require: [0]
|
||||
guard-for-in: [2]
|
||||
guard-for-in: [0] # not needed for our non-OOP stuff
|
||||
handle-callback-err: [2, ^(err|error)$]
|
||||
id-blacklist: [0]
|
||||
id-length: [0]
|
||||
id-match: [0]
|
||||
indent: [2, 2, {VariableDeclarator: 0}]
|
||||
indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}]
|
||||
jsx-quotes: [0]
|
||||
key-spacing: [0]
|
||||
keyword-spacing: [2]
|
||||
linebreak-style: [2, unix]
|
||||
lines-around-comment: [0]
|
||||
lines-around-directive: [0]
|
||||
max-len: [2, {code: 120, ignoreComments: true, ignoreRegExpLiterals: true}]
|
||||
|
@ -98,7 +113,7 @@ rules:
|
|||
no-case-declarations: [2]
|
||||
no-class-assign: [2]
|
||||
no-cond-assign: [2, except-parens]
|
||||
no-confusing-arrow: [2]
|
||||
no-confusing-arrow: [1, {allowParens: true}]
|
||||
no-const-assign: [2]
|
||||
no-constant-condition: [0]
|
||||
no-continue: [0]
|
||||
|
@ -125,11 +140,11 @@ rules:
|
|||
no-extra-label: [0]
|
||||
no-extra-parens: [0]
|
||||
no-extra-semi: [2]
|
||||
no-fallthrough: [2]
|
||||
no-fallthrough: [2, {commentPattern: fallthrough.*}]
|
||||
no-floating-decimal: [0]
|
||||
no-func-assign: [2]
|
||||
no-global-assign: [2]
|
||||
no-implicit-coercion: [2]
|
||||
no-implicit-coercion: [1]
|
||||
no-implicit-globals: [0]
|
||||
no-implied-eval: [2]
|
||||
no-inline-comments: [0]
|
||||
|
@ -139,7 +154,7 @@ rules:
|
|||
no-irregular-whitespace: [2]
|
||||
no-iterator: [2]
|
||||
no-label-var: [2]
|
||||
no-labels: [2]
|
||||
no-labels: [2, {allowLoop: true}]
|
||||
no-lone-blocks: [2]
|
||||
no-lonely-if: [0]
|
||||
no-loop-func: [0]
|
||||
|
@ -149,7 +164,7 @@ rules:
|
|||
no-mixed-spaces-and-tabs: [2]
|
||||
no-multi-spaces: [0]
|
||||
no-multi-str: [2]
|
||||
no-multiple-empty-lines: [2, {max: 1, maxEOF: 0, maxBOF: 0}]
|
||||
no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}]
|
||||
no-native-reassign: [2]
|
||||
no-negated-condition: [0]
|
||||
no-negated-in-lhs: [2]
|
||||
|
@ -184,43 +199,44 @@ rules:
|
|||
no-tabs: [2]
|
||||
no-template-curly-in-string: [2]
|
||||
no-this-before-super: [2]
|
||||
no-throw-literal: [2]
|
||||
no-throw-literal: [0]
|
||||
no-trailing-spaces: [2]
|
||||
no-undef-init: [2]
|
||||
no-undef: [2]
|
||||
no-undefined: [0]
|
||||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
no-unmodified-loop-condition: [2]
|
||||
no-unmodified-loop-condition: [1]
|
||||
no-unneeded-ternary: [2]
|
||||
no-unreachable: [2]
|
||||
no-unsafe-finally: [2]
|
||||
no-unsafe-negation: [2]
|
||||
no-unused-expressions: [2]
|
||||
no-unused-labels: [0]
|
||||
no-unused-vars: [2, {args: all, varsIgnorePattern: clearError, argsIgnorePattern: ^_}]
|
||||
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
||||
no-use-before-define: [2, nofunc]
|
||||
no-useless-call: [2]
|
||||
no-useless-computed-key: [2]
|
||||
no-useless-concat: [2]
|
||||
no-useless-constructor: [2]
|
||||
no-useless-escape: [2]
|
||||
no-var: [0]
|
||||
no-var: [1]
|
||||
no-warning-comments: [0]
|
||||
no-whitespace-before-property: [2]
|
||||
no-with: [2]
|
||||
object-curly-newline: [0]
|
||||
object-curly-spacing: [2, never]
|
||||
object-shorthand: [0]
|
||||
one-var-declaration-per-line: [0]
|
||||
one-var-declaration-per-line: [1]
|
||||
one-var: [0]
|
||||
operator-assignment: [2, always]
|
||||
operator-linebreak: [2, after]
|
||||
operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}]
|
||||
padded-blocks: [2, never]
|
||||
prefer-numeric-literals: [2]
|
||||
prefer-rest-params: [0]
|
||||
prefer-const: [1, {destructuring: any, ignoreReadBeforeAssign: true}]
|
||||
quote-props: [0]
|
||||
quotes: [2, double, avoid-escape]
|
||||
quotes: [1, single, avoid-escape]
|
||||
radix: [2, as-needed]
|
||||
require-jsdoc: [0]
|
||||
require-yield: [2]
|
||||
|
@ -233,7 +249,7 @@ rules:
|
|||
space-in-parens: [2, never]
|
||||
space-infix-ops: [2]
|
||||
space-unary-ops: [2]
|
||||
spaced-comment: [2, always, {markers: ["!"]}]
|
||||
spaced-comment: [0, always, {markers: ["!"]}]
|
||||
strict: [2, global]
|
||||
symbol-description: [2]
|
||||
template-curly-spacing: [2, never]
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "هل تريد بالتأكيد حذف هذا النمط؟",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "إزالة القسم",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "výchozí",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Pouze upravené styly.",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Exportovat",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Získat styly na userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Získat pomoc</a>",
|
||||
"message": "<a href='https://userstyles.org'>Získat styly na userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Získat pomoc</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Opravdu chcete tento styl smazat?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla Formát",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus nefunguje na těchto stránkách.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus nefunguje na těchto stránkách.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
"message": "Styles Exportieren",
|
||||
"description": ""
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Nur bearbeitete Styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "Automatischer Update- und Installations-Intervall (in Stunden)",
|
||||
"description": ""
|
||||
|
@ -83,7 +79,7 @@
|
|||
"message": "Hilfe",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Löschen",
|
||||
"description": ""
|
||||
},
|
||||
|
@ -169,7 +165,7 @@
|
|||
"description": ""
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Styles von userstyles.org beziehen</a><br><a href='https://userstyles.org/help/stylish_chrome'>Hilfeseite anzeigen</a>",
|
||||
"message": "<a href='https://userstyles.org'>Styles von userstyles.org beziehen</a> | <a href='http://add0n.com/stylus.html'>Hilfeseite anzeigen</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -356,8 +352,8 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus funktioniert nicht auf Seiten wie diesen.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus funktioniert nicht auf Seiten wie diesen.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Μόνο επεξεργασμενα στυλ",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"appliesLabel": {
|
||||
"message": "Applies to",
|
||||
"description": "Label for 'applies to' fields on the edit\/add screen"
|
||||
"description": "Label for 'applies to' fields on the edit/add screen"
|
||||
},
|
||||
"appliesRegexpOption": {
|
||||
"message": "URLs matching the regexp",
|
||||
|
@ -76,6 +76,13 @@
|
|||
"message": "Check all styles for updates",
|
||||
"description": "Label for the button to check all styles for updates"
|
||||
},
|
||||
"checkAllUpdatesForce": {
|
||||
"message": "Check again, I didn't edit any styles!",
|
||||
"description": "Label for the button to apply all detected updates"
|
||||
},
|
||||
"updateCheckHistory": {
|
||||
"message": "History of update checks"
|
||||
},
|
||||
"checkForUpdate": {
|
||||
"message": "Check for update",
|
||||
"description": "Label for the button to check a single style for an update"
|
||||
|
@ -108,6 +115,26 @@
|
|||
"message": "Theme",
|
||||
"description": "Label for the style editor's CSS theme."
|
||||
},
|
||||
"cm_matchHighlight": {
|
||||
"message": "Highlight",
|
||||
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
|
||||
},
|
||||
"cm_matchHighlightToken": {
|
||||
"message": "Token under cursor",
|
||||
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of of the word/token under cursor even if nothing is selected"
|
||||
},
|
||||
"cm_matchHighlightSelection": {
|
||||
"message": "Selection only",
|
||||
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
|
||||
},
|
||||
"genericDisabledLabel": {
|
||||
"message": "Disabled",
|
||||
"description": "Used in various lists/options to indicate that something is disabled"
|
||||
},
|
||||
"genericHistoryLabel": {
|
||||
"message": "History",
|
||||
"description": "Used in various places to show a history log of something"
|
||||
},
|
||||
"confirmNo": {
|
||||
"message": "No",
|
||||
"description": "'No' button in a confirm dialog"
|
||||
|
@ -141,6 +168,9 @@
|
|||
"description": "Drag'n'drop message"
|
||||
},
|
||||
"confirmOK": {
|
||||
"message": "OK"
|
||||
},
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -191,6 +221,10 @@
|
|||
"message": "Enable",
|
||||
"description": "Label for the button to enable a style"
|
||||
},
|
||||
"editDeleteText": {
|
||||
"message": "Delete",
|
||||
"description": "Label for the context menu item in the editor to delete selected text"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -211,6 +245,46 @@
|
|||
"message": "Type a command name",
|
||||
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
|
||||
},
|
||||
"importReportLegendAdded": {
|
||||
"message": "added",
|
||||
"description": "Text after the number of styles added in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendIdentical": {
|
||||
"message": "identical skipped",
|
||||
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendInvalid": {
|
||||
"message": "invalid skipped",
|
||||
"description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedBoth": {
|
||||
"message": "updated both meta info and code",
|
||||
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedCode": {
|
||||
"message": "updated code",
|
||||
"description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles"
|
||||
},
|
||||
"importReportLegendUpdatedMeta": {
|
||||
"message": "updated meta info",
|
||||
"description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles"
|
||||
},
|
||||
"importReportTitle": {
|
||||
"message": "Finished importing styles",
|
||||
"description": "Title of the report shown after importing styles"
|
||||
},
|
||||
"importReportUnchanged": {
|
||||
"message": "Nothing was changed.",
|
||||
"description": "Message in the report shown after importing styles"
|
||||
},
|
||||
"importReportUndoneTitle": {
|
||||
"message": "Import has been undone",
|
||||
"description": "Title of the message box shown after undoing the import of styles"
|
||||
},
|
||||
"importReportUndone": {
|
||||
"message": "styles were reverted",
|
||||
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
|
||||
},
|
||||
"importLabel": {
|
||||
"message": "Import",
|
||||
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -240,7 +314,7 @@
|
|||
"description": "Label for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"issuesHelp": {
|
||||
"message": "The issues found by <a href='https:\/\/github.com\/CSSLint\/csslint' target='_blank'>CSSLint<\/a> with these rules enabled:",
|
||||
"message": "The issues found by <a href='https://github.com/CSSLint/csslint' target='_blank'>CSSLint</a> with these rules enabled:",
|
||||
"description": "Help popup message for the CSSLint issues block on the style edit page"
|
||||
},
|
||||
"manageFilters": {
|
||||
|
@ -255,12 +329,40 @@
|
|||
"message": "Only enabled styles",
|
||||
"description": "Checkbox to show only enabled styles"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
"manageOnlyLocal": {
|
||||
"message": "Only locally created styles",
|
||||
"description": "Checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyLocalTooltip": {
|
||||
"message": "(the styles not installed through a userstyles.org page)",
|
||||
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
|
||||
},
|
||||
"manageOnlyUpdates": {
|
||||
"message": "Only with updates or issues",
|
||||
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
|
||||
},
|
||||
"manageNewUI": {
|
||||
"message": "New manage UI layout",
|
||||
"description": "Label for the checkbox that toggles the new UI on manage page"
|
||||
},
|
||||
"manageFavicons": {
|
||||
"message": "Favicons in applies-to column",
|
||||
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageFaviconsGray": {
|
||||
"message": "Grayed out",
|
||||
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageFaviconsHelp": {
|
||||
"message": "Stylus uses an external service https://www.google.com/s2/favicons",
|
||||
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
|
||||
},
|
||||
"manageMaxTargets": {
|
||||
"message": "Number of applies-to items",
|
||||
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https:\/\/userstyles.org'>Get styles on userstyles.org<\/a> | <a href='https:\/\/userstyles.org\/help\/stylish_chrome'>Get help<\/a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"manageTitle": {
|
||||
|
@ -287,14 +389,6 @@
|
|||
"message": "Options",
|
||||
"description": "Go to Options UI"
|
||||
},
|
||||
"openOptionsShortcuts": {
|
||||
"message": "Shortcuts",
|
||||
"description": "Go to shortcut configuration"
|
||||
},
|
||||
"openShortcutsPopup": {
|
||||
"message": "Shortcuts",
|
||||
"description": "Go to shortcut configuration"
|
||||
},
|
||||
"optionsHeading": {
|
||||
"message": "Options",
|
||||
"description": "Heading for options section on manage page."
|
||||
|
@ -304,11 +398,11 @@
|
|||
"description": "Subheading for options section on manage page."
|
||||
},
|
||||
"popupStylesFirst": {
|
||||
"message": "List styles before commands in the toolbar button menu",
|
||||
"description": "Label for the checkbox controlling section order in the toolbar button menu."
|
||||
"message": "Styles before commands",
|
||||
"description": "Label for the checkbox controlling section order in the popup."
|
||||
},
|
||||
"prefShowBadge": {
|
||||
"message": "Show number of styles active for the current site on the toolbar button",
|
||||
"message": "Number of styles active for the current site",
|
||||
"description": "Label for the checkbox controlling toolbar badge text."
|
||||
},
|
||||
"replace": {
|
||||
|
@ -351,10 +445,55 @@
|
|||
"message": "Remove section",
|
||||
"description": "Label for the button to remove a section"
|
||||
},
|
||||
"shortcuts": {
|
||||
"message": "Shortcuts",
|
||||
"description": "Go to shortcut configuration"
|
||||
},
|
||||
"shortcutsNote": {
|
||||
"message": "Define keyboard shortcuts"
|
||||
},
|
||||
"styleBadRegexp": {
|
||||
"message": "Regexp is invalid.",
|
||||
"description": "Validation message for a bad regexp in a style"
|
||||
},
|
||||
"styleRegexpTestButton": {
|
||||
"message": "RegExp test",
|
||||
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
|
||||
},
|
||||
"styleRegexpTestTitle": {
|
||||
"message": "List of matching opened tabs (click on URL to focus its tab)",
|
||||
"description": "RegExp test report: title of the report"
|
||||
},
|
||||
"styleRegexpTestFull": {
|
||||
"message": "Matching tabs",
|
||||
"description": "RegExp test report: label for the fully matching expressions"
|
||||
},
|
||||
"styleRegexpTestPartial": {
|
||||
"message": "Not matching fully, hence skipped",
|
||||
"description": "RegExp test report: label for the partially matching expressions"
|
||||
},
|
||||
"styleRegexpTestNone": {
|
||||
"message": "No matching tabs",
|
||||
"description": "RegExp test report: label for expressions that didn't match any tabs"
|
||||
},
|
||||
"styleRegexpTestInvalid": {
|
||||
"message": "Invalid regexps skipped",
|
||||
"description": "RegExp test report: label for the invalid expressions"
|
||||
},
|
||||
"styleRegexpPartialExplanation": {
|
||||
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome which incorrectly checks 'regexp()' rules since the very first version (known bug)."
|
||||
},
|
||||
"styleRegexpInvalidExplanation": {
|
||||
"message": "Some 'regexp()' rules that could not be compiled at all."
|
||||
},
|
||||
"styleNotAppliedRegexpProblemTooltip": {
|
||||
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
|
||||
"description": "Tooltip in the popup for styles that were not applied at all"
|
||||
},
|
||||
"styleRegexpProblemTooltip": {
|
||||
"message": "Number of sections not applied due to incorrect usage of 'regexp()'",
|
||||
"description": "Tooltip in the popup for styles that were applied only partially"
|
||||
},
|
||||
"styleBeautify": {
|
||||
"message": "Beautify",
|
||||
"description": "Label for the CSS-beautifier button on the edit style page"
|
||||
|
@ -417,10 +556,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus can't affect this page.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus doesn't work on pages like this.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"stylusUnavailableForURLdetails": {
|
||||
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version or about:addons) as well as other extensions' pages. Chrome/Chromium forks also restrict the Chrome Web Store.",
|
||||
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"toggleStyle": {
|
||||
"message": "Toggle style",
|
||||
"description": "Label for the checkbox to enable/disable a style"
|
||||
},
|
||||
"undo": {
|
||||
"message": "Undo",
|
||||
"description": "Button label"
|
||||
|
@ -429,6 +576,14 @@
|
|||
"message": "Undo (global)",
|
||||
"description": "CSS-beautify global Undo button label"
|
||||
},
|
||||
"unreachableContentScript": {
|
||||
"message": "Could not communicate with the page. Try reloading the tab.",
|
||||
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
|
||||
},
|
||||
"unreachableFileHint": {
|
||||
"message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.",
|
||||
"description": "Note in the toolbar popup for file:// URLs"
|
||||
},
|
||||
"updateCheckFailBadResponseCode": {
|
||||
"message": "Update failed - server responded with code $code$.",
|
||||
"description": "Text that displays when an update check failed because the response code indicates an error",
|
||||
|
@ -442,18 +597,42 @@
|
|||
"message": "Update failed - server unreachable.",
|
||||
"description": "Text that displays when an update check failed because the update server is unreachable"
|
||||
},
|
||||
"updateCheckSkippedLocallyEdited": {
|
||||
"message": "This style was edited locally.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSkippedMaybeLocallyEdited": {
|
||||
"message": "This style might have been edited locally.",
|
||||
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
|
||||
},
|
||||
"updateCheckManualUpdateForce": {
|
||||
"message": "Install update (local edits will be overwritten)",
|
||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckManualUpdateHint": {
|
||||
"message": "Forcing an update will overwrite any local edits.",
|
||||
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
|
||||
},
|
||||
"updateCheckSucceededNoUpdate": {
|
||||
"message": "Style is up to date.",
|
||||
"description": "Text that displays when an update check completed and no update is available"
|
||||
},
|
||||
"updateAllCheckSucceededNoUpdate": {
|
||||
"message": "All styles are up to date.",
|
||||
"message": "No updates found.",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateAllCheckSucceededSomeEdited": {
|
||||
"message": "Some updatable styles weren't checked to avoid losing possible local edits. Updates can be forced by checking individually, or by running another check for all styles (local edits will be overwritten).",
|
||||
"description": "Text that displays when an update all check completed and no updates are available"
|
||||
},
|
||||
"updateCompleted": {
|
||||
"message": "Update completed.",
|
||||
"description": "Text that displays when an update completed"
|
||||
},
|
||||
"updatesCurrentlyInstalled": {
|
||||
"message": "Updates installed:",
|
||||
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
|
||||
},
|
||||
"writeStyleFor": {
|
||||
"message": "Write style for: ",
|
||||
"description": "Label for toolbar pop-up that precedes the links to write a new style"
|
||||
|
@ -469,10 +648,10 @@
|
|||
"message": "Import styles"
|
||||
},
|
||||
"optionsBadgeNormal": {
|
||||
"message": "Badge background color"
|
||||
"message": "Background color"
|
||||
},
|
||||
"optionsBadgeDisabled": {
|
||||
"message": "Badge background color (when disabled)"
|
||||
"message": "Background color when disabled"
|
||||
},
|
||||
"optionsPopupWidth": {
|
||||
"message": "Popup width (in pixels)"
|
||||
|
@ -481,19 +660,43 @@
|
|||
"message": "Automatically check for and install all available userstyle updates (in hrs)"
|
||||
},
|
||||
"optionsUpdateIntervalNote": {
|
||||
"message": "To disable the automatic userstyle update checks, set interval to zero"
|
||||
"message": "To disable the automatic userstyle update checks, set interval to 0"
|
||||
},
|
||||
"optionsCustomize": {
|
||||
"message": "UI Customizations"
|
||||
"optionsUpdateImportNote": {
|
||||
"message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
|
||||
},
|
||||
"optionsCustomizeBadge": {
|
||||
"message": "Badge on the toolbar icon"
|
||||
},
|
||||
"optionsCustomizePopup": {
|
||||
"message": "Popup"
|
||||
},
|
||||
"optionsCustomizeUpdate": {
|
||||
"message": "Updates"
|
||||
},
|
||||
"optionsAdvanced": {
|
||||
"message": "Advanced"
|
||||
},
|
||||
"optionsAdvancedExposeIframes": {
|
||||
"message": "Expose iframes via HTML[stylus-iframe]"
|
||||
},
|
||||
"optionsAdvancedExposeIframesNote": {
|
||||
"message": "Enables writing iframe-specific CSS like 'html[stylus-iframe] h1 { display:none }'"
|
||||
},
|
||||
"optionsAdvancedContextDelete": {
|
||||
"message": "Add 'Delete' in editor context menu"
|
||||
},
|
||||
"optionsActions": {
|
||||
"message": "Actions"
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Open styles manager"
|
||||
"optionsReset": {
|
||||
"message": "Reset the options to default values"
|
||||
},
|
||||
"optionsOpenManagerNote": {
|
||||
"message": "Define a keyboard shortcut"
|
||||
"optionsResetButton": {
|
||||
"message": "Reset options"
|
||||
},
|
||||
"optionsOpenManager": {
|
||||
"message": "Manage styles"
|
||||
},
|
||||
"optionsCheckUpdate": {
|
||||
"message": "Check for and install all available updates"
|
||||
|
@ -502,6 +705,6 @@
|
|||
"message": "Open"
|
||||
},
|
||||
"optionsCheck": {
|
||||
"message": "Check"
|
||||
"message": "Update styles"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
"message": "Exportar estilos",
|
||||
"description": ""
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Sólo estilos editados",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "Buscar e instalar automáticamente todas las actualizaciones disponibles de estilos de usuario (en horas)",
|
||||
"description": ""
|
||||
|
@ -157,7 +153,7 @@
|
|||
"description": ""
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Obtener estilos en userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Obtener ayuda</a>",
|
||||
"message": "<a href='https://userstyles.org'>Obtener estilos en userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Obtener ayuda</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -212,7 +208,7 @@
|
|||
"message": "¿Está seguro de que quiere eliminar este estilo?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -350,8 +346,8 @@
|
|||
"message": "Formato Mozilla",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus no funciona en páginas como esta)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus no funciona en páginas como esta",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Oletko varma että haluat poistaa tämän tyylin?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Poista osio",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "défaut",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Exportez",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Voulez-vous vraiment supprimer ce style ?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus ne fonctionne pas sur les pages de ce genre)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus ne fonctionne pas sur les pages de ce genre",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Vuoi eliminare questo stile?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Rimuovi sezione",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "このスタイルを削除してもよろしいですか?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "セクションを削除",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "standaard",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Alleen bewerkte stijlen",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Exporteren",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Verkrijg stijlen op userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Verkrijg hulp</a>",
|
||||
"message": "<a href='https://userstyles.org'>Verkrijg stijlen op userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Verkrijg hulp</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Weet u zeker dat u deze stijl wilt verwijderen?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla-opmaak",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus werkt niet op pagina's als deze.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus werkt niet op pagina's als deze.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Tem certeza de que deseja excluir este estilo?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Remover seção",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "по-умолчанию",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Только отредактированные стили",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Экспорт",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Скачать стили с userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Справка</a>",
|
||||
"message": "<a href='https://userstyles.org'>Скачать стили с userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Справка</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Удалить этот стиль?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Формат Mozilla",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus не работает на таких страницах)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus не работает на таких страницах.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "подразумевано",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Само уређени стилови",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Извези",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Помоћ</a>",
|
||||
"message": "<a href='https://userstyles.org'>Преузмите стилове са userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Помоћ</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Да ли сте сигурни да желите да избришете овај стил?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla формат",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus не ради на страницама као што је ова.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus не ради на страницама као што је ова.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Endast ändrade stilar",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Är du säker på att du vill ta bort denna stil?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus fungerar inte på sidor som denna.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus fungerar inte på sidor som denna.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Endast ändrade stilar",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Är du säker på att du vill ta bort denna stil?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus fungerar inte på sidor som dessa.)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus fungerar inte på sidor som dessa.",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Remove section",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "Bu stili silmek istediğinizden emin misiniz?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Bölümü kaldır",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "default",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "Only edited styles",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "Export",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "确定要删除这个样式吗?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,10 +290,6 @@
|
|||
"message": "Mozilla Format",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus does not work on pages like this.)",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "移除节",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
|
|
@ -11,10 +11,6 @@
|
|||
"message": "导出所有样式",
|
||||
"description": ""
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "仅修改过的样式",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"optionsUpdateInterval": {
|
||||
"message": "每 N 小时,检查所有样式更新(0 为关闭检查)",
|
||||
"description": ""
|
||||
|
@ -79,7 +75,7 @@
|
|||
"message": "帮助",
|
||||
"description": "Alternate text for help buttons"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "确定",
|
||||
"description": ""
|
||||
},
|
||||
|
@ -165,7 +161,7 @@
|
|||
"description": ""
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>访问 userstyles.org 获取样式</a> | <a href='https://userstyles.org/help/stylish_chrome'>获取帮助</a>",
|
||||
"message": "<a href='https://userstyles.org'>访问 userstyles.org 获取样式</a> | <a href='http://add0n.com/stylus.html'>获取帮助</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -352,8 +348,8 @@
|
|||
"message": "Mozilla 格式",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "(Stylus在这样的页面上不工作)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus在这样的页面上不工作",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
"message": "默認",
|
||||
"description": "Default CodeMirror CSS theme option on the edit style page"
|
||||
},
|
||||
"manageOnlyEdited": {
|
||||
"message": "只顯示已禁用的樣式",
|
||||
"description": "Checkbox to show only locally edited styles"
|
||||
},
|
||||
"exportLabel": {
|
||||
"message": "導出",
|
||||
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
|
||||
|
@ -117,7 +113,7 @@
|
|||
}
|
||||
},
|
||||
"manageText": {
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='https://userstyles.org/help/stylish_chrome'>Get help</a>",
|
||||
"message": "<a href='https://userstyles.org'>Get styles on userstyles.org</a> | <a href='http://add0n.com/stylus.html'>Get help</a>",
|
||||
"description": "Help text on the manage page"
|
||||
},
|
||||
"searchStyles": {
|
||||
|
@ -168,7 +164,7 @@
|
|||
"message": "確定要刪除這個樣式嗎?",
|
||||
"description": "Confirmation before deleting a style"
|
||||
},
|
||||
"confirmOK": {
|
||||
"confirmDelete": {
|
||||
"message": "Delete"
|
||||
},
|
||||
"confirmCancel": {
|
||||
|
@ -294,8 +290,8 @@
|
|||
"message": "Mozilla格式",
|
||||
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
|
||||
},
|
||||
"stylishUnavailableForURL": {
|
||||
"message": "( Stylus 不能在諸如此類的網頁上生效。)",
|
||||
"stylusUnavailableForURL": {
|
||||
"message": "Stylus 不能在諸如此類的網頁上生效。",
|
||||
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
|
||||
},
|
||||
"sectionRemove": {
|
||||
|
|
628
apply.js
|
@ -1,352 +1,336 @@
|
|||
// using ES5 syntax because ES6 is fast only since around Chrome 55
|
||||
// so we'll wait until Chrome 60 arguably before converting
|
||||
// Not using some slow features of ES6, see http://kpdecker.github.io/six-speed/
|
||||
// like destructring, classes, defaults, spread, calculated key names
|
||||
/* eslint no-var: 0 */
|
||||
'use strict';
|
||||
|
||||
var g_disableAll = false;
|
||||
var g_styleElements = {};
|
||||
var iframeObserver;
|
||||
var retiredStyleIds = [];
|
||||
var ID_PREFIX = 'stylus-';
|
||||
var ROOT = document.documentElement;
|
||||
var isOwnPage = location.href.startsWith('chrome-extension:');
|
||||
var disableAll = false;
|
||||
var exposeIframes = false;
|
||||
var styleElements = new Map();
|
||||
var disabledElements = new Map();
|
||||
var retiredStyleTimers = new Map();
|
||||
var docRewriteObserver;
|
||||
|
||||
initObserver();
|
||||
requestStyles();
|
||||
|
||||
function requestStyles() {
|
||||
// If this is a Stylish page (Edit Style or Manage Styles),
|
||||
// we'll request the styles directly to minimize delay and flicker,
|
||||
// unless Chrome still starts up and the background page isn't fully loaded.
|
||||
// (Note: in this case the function may be invoked again from applyStyles.)
|
||||
var request = {method: "getStyles", matchUrl: location.href, enabled: true, asHash: true};
|
||||
if (location.href.indexOf(chrome.extension.getURL("")) == 0) {
|
||||
var bg = chrome.extension.getBackgroundPage();
|
||||
if (bg && bg.getStyles) {
|
||||
// apply styles immediately, then proceed with a normal request that will update the icon
|
||||
bg.getStyles(request, applyStyles);
|
||||
}
|
||||
}
|
||||
chrome.runtime.sendMessage(request, applyStyles);
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||
|
||||
if (!isOwnPage) {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
}
|
||||
|
||||
function requestStyles(options, callback = applyStyles) {
|
||||
var matchUrl = location.href;
|
||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||
// dynamic about: and javascript: iframes don't have an URL yet
|
||||
// so we'll try the parent frame which is guaranteed to have a real URL
|
||||
try {
|
||||
if (window != parent) {
|
||||
matchUrl = parent.location.href;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
const request = Object.assign({
|
||||
method: 'getStyles',
|
||||
matchUrl,
|
||||
enabled: true,
|
||||
asHash: true,
|
||||
}, options);
|
||||
// On own pages we request the styles directly to minimize delay and flicker
|
||||
if (typeof getStylesSafe !== 'undefined') {
|
||||
getStylesSafe(request).then(callback);
|
||||
} else {
|
||||
chrome.runtime.sendMessage(request, callback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applyOnMessage(request, sender, sendResponse) {
|
||||
// Also handle special request just for the pop-up
|
||||
switch (request.method == "updatePopup" ? request.reason : request.method) {
|
||||
case "styleDeleted":
|
||||
removeStyle(request.id, document);
|
||||
break;
|
||||
case "styleUpdated":
|
||||
if (request.style.enabled) {
|
||||
retireStyle(request.style.id);
|
||||
// fallthrough to "styleAdded"
|
||||
} else {
|
||||
removeStyle(request.style.id, document);
|
||||
break;
|
||||
}
|
||||
case "styleAdded":
|
||||
if (request.style.enabled) {
|
||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: location.href, enabled: true, id: request.style.id, asHash: true}, applyStyles);
|
||||
}
|
||||
break;
|
||||
case "styleApply":
|
||||
applyStyles(request.styles);
|
||||
break;
|
||||
case "styleReplaceAll":
|
||||
replaceAll(request.styles, document);
|
||||
break;
|
||||
case "styleDisableAll":
|
||||
disableAll(request.disableAll);
|
||||
break;
|
||||
case "ping":
|
||||
sendResponse(true);
|
||||
break;
|
||||
}
|
||||
if (request.styles == 'DIY') {
|
||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||
// which is faster because IPC messaging JSON-ifies everything internally
|
||||
requestStyles({}, styles => {
|
||||
request.styles = styles;
|
||||
applyOnMessage(request);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
|
||||
case 'styleDeleted':
|
||||
removeStyle(request);
|
||||
break;
|
||||
|
||||
case 'styleUpdated':
|
||||
if (request.codeIsUpdated === false) {
|
||||
applyStyleState(request.style);
|
||||
break;
|
||||
}
|
||||
if (request.style.enabled) {
|
||||
removeStyle({id: request.style.id, retire: true});
|
||||
requestStyles({id: request.style.id});
|
||||
} else {
|
||||
removeStyle(request.style);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleAdded':
|
||||
if (request.style.enabled) {
|
||||
requestStyles({id: request.style.id});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleApply':
|
||||
applyStyles(request.styles);
|
||||
break;
|
||||
|
||||
case 'styleReplaceAll':
|
||||
replaceAll(request.styles);
|
||||
break;
|
||||
|
||||
case 'prefChanged':
|
||||
if ('disableAll' in request.prefs) {
|
||||
doDisableAll(request.prefs.disableAll);
|
||||
}
|
||||
if ('exposeIframes' in request.prefs) {
|
||||
doExposeIframes(request.prefs.exposeIframes);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
sendResponse(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function disableAll(disable) {
|
||||
if (!disable === !g_disableAll) {
|
||||
return;
|
||||
}
|
||||
g_disableAll = disable;
|
||||
if (g_disableAll) {
|
||||
iframeObserver.disconnect();
|
||||
}
|
||||
|
||||
disableSheets(g_disableAll, document);
|
||||
|
||||
if (!g_disableAll && document.readyState != "loading") {
|
||||
iframeObserver.start();
|
||||
}
|
||||
|
||||
function disableSheets(disable, doc) {
|
||||
Array.prototype.forEach.call(doc.styleSheets, function(stylesheet) {
|
||||
if (stylesheet.ownerNode.classList.contains("stylus")) {
|
||||
stylesheet.disabled = disable;
|
||||
}
|
||||
});
|
||||
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||
if (!disable) {
|
||||
// update the IFRAME if it was created while the observer was disconnected
|
||||
addDocumentStylesToIFrame(iframe);
|
||||
}
|
||||
disableSheets(disable, iframe.contentDocument);
|
||||
});
|
||||
}
|
||||
function doDisableAll(disable = disableAll) {
|
||||
if (!disable === !disableAll) {
|
||||
return;
|
||||
}
|
||||
disableAll = disable;
|
||||
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||
if (stylesheet.ownerNode.matches(`STYLE.stylus[id^="${ID_PREFIX}"]`)
|
||||
&& stylesheet.disabled != disable) {
|
||||
stylesheet.disabled = disable;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeStyle(id, doc) {
|
||||
var e = doc.getElementById("stylus-" + id);
|
||||
delete g_styleElements["stylus-" + id];
|
||||
if (e) {
|
||||
e.remove();
|
||||
}
|
||||
if (doc == document && Object.keys(g_styleElements).length == 0) {
|
||||
iframeObserver.disconnect();
|
||||
}
|
||||
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||
removeStyle(id, iframe.contentDocument);
|
||||
});
|
||||
|
||||
function doExposeIframes(state = exposeIframes) {
|
||||
if (state === exposeIframes || window == parent) {
|
||||
return;
|
||||
}
|
||||
exposeIframes = state;
|
||||
const attr = document.documentElement.getAttribute('stylus-iframe');
|
||||
if (state && attr != '') {
|
||||
document.documentElement.setAttribute('stylus-iframe', '');
|
||||
} else if (!state && attr == '') {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
}
|
||||
}
|
||||
|
||||
// to avoid page flicker when the style is updated
|
||||
// instead of removing it immediately we rename its ID and queue it
|
||||
// to be deleted in applyStyles after a new version is fetched and applied
|
||||
function retireStyle(id, doc) {
|
||||
var deadID = "ghost-" + id;
|
||||
if (!doc) {
|
||||
doc = document;
|
||||
retiredStyleIds.push(deadID);
|
||||
delete g_styleElements["stylus-" + id];
|
||||
// in case something went wrong and new style was never applied
|
||||
setTimeout(removeStyle.bind(null, deadID, doc), 1000);
|
||||
}
|
||||
var e = doc.getElementById("stylus-" + id);
|
||||
if (e) {
|
||||
e.id = "stylus-" + deadID;
|
||||
}
|
||||
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||
retireStyle(id, iframe.contentDocument);
|
||||
});
|
||||
|
||||
function applyStyleState({id, enabled}) {
|
||||
const inCache = disabledElements.get(id) || styleElements.get(id);
|
||||
const inDoc = document.getElementById(ID_PREFIX + id);
|
||||
if (enabled) {
|
||||
if (inDoc) {
|
||||
return;
|
||||
} else if (inCache) {
|
||||
addStyleElement(inCache);
|
||||
disabledElements.delete(id);
|
||||
} else {
|
||||
requestStyles({id});
|
||||
}
|
||||
} else {
|
||||
if (inDoc) {
|
||||
disabledElements.set(id, inDoc);
|
||||
inDoc.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyStyles(styleHash) {
|
||||
if (!styleHash) { // Chrome is starting up
|
||||
requestStyles();
|
||||
return;
|
||||
}
|
||||
if ("disableAll" in styleHash) {
|
||||
disableAll(styleHash.disableAll);
|
||||
delete styleHash.disableAll;
|
||||
}
|
||||
|
||||
for (var styleId in styleHash) {
|
||||
applySections(styleId, styleHash[styleId]);
|
||||
}
|
||||
|
||||
if (Object.keys(g_styleElements).length) {
|
||||
// when site response is application/xml Chrome displays our style elements
|
||||
// under document.documentElement as plain text so we need to move them into HEAD
|
||||
// (which already is autogenerated at this moment for the xml response)
|
||||
if (document.head && document.head.firstChild && document.head.firstChild.id == "xml-viewer-style") {
|
||||
for (var id in g_styleElements) {
|
||||
document.head.appendChild(document.getElementById(id));
|
||||
}
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", onDOMContentLoaded);
|
||||
}
|
||||
|
||||
if (retiredStyleIds.length) {
|
||||
setTimeout(function() {
|
||||
while (retiredStyleIds.length) {
|
||||
removeStyle(retiredStyleIds.shift(), document);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
function removeStyle({id, retire = false}) {
|
||||
const el = document.getElementById(ID_PREFIX + id);
|
||||
if (el) {
|
||||
if (retire) {
|
||||
// to avoid page flicker when the style is updated
|
||||
// instead of removing it immediately we rename its ID and queue it
|
||||
// to be deleted in applyStyles after a new version is fetched and applied
|
||||
const deadID = 'ghost-' + id;
|
||||
el.id = ID_PREFIX + deadID;
|
||||
// in case something went wrong and new style was never applied
|
||||
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
||||
} else {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
styleElements.delete(ID_PREFIX + id);
|
||||
disabledElements.delete(id);
|
||||
retiredStyleTimers.delete(id);
|
||||
}
|
||||
|
||||
function onDOMContentLoaded() {
|
||||
addDocumentStylesToAllIFrames();
|
||||
iframeObserver.start();
|
||||
|
||||
function applyStyles(styles) {
|
||||
if (!styles) {
|
||||
// Chrome is starting up
|
||||
requestStyles();
|
||||
return;
|
||||
}
|
||||
if ('disableAll' in styles) {
|
||||
doDisableAll(styles.disableAll);
|
||||
delete styles.disableAll;
|
||||
}
|
||||
if ('exposeIframes' in styles) {
|
||||
doExposeIframes(styles.exposeIframes);
|
||||
delete styles.exposeIframes;
|
||||
}
|
||||
if (document.head
|
||||
&& document.head.firstChild
|
||||
&& document.head.firstChild.id == 'xml-viewer-style') {
|
||||
// when site response is application/xml Chrome displays our style elements
|
||||
// under document.documentElement as plain text so we need to move them into HEAD
|
||||
// which is already autogenerated at this moment
|
||||
ROOT = document.head;
|
||||
}
|
||||
for (const id in styles) {
|
||||
applySections(id, styles[id]);
|
||||
}
|
||||
initDocRewriteObserver();
|
||||
if (retiredStyleTimers.size) {
|
||||
setTimeout(() => {
|
||||
for (const [id, timer] of retiredStyleTimers.entries()) {
|
||||
removeStyle({id});
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function applySections(styleId, sections) {
|
||||
var styleElement = document.getElementById("stylus-" + styleId);
|
||||
// Already there.
|
||||
if (styleElement) {
|
||||
return;
|
||||
}
|
||||
if (document.documentElement instanceof SVGSVGElement) {
|
||||
// SVG document, make an SVG style element.
|
||||
styleElement = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
||||
} else {
|
||||
// This will make an HTML style element. If there's SVG embedded in an HTML document, this works on the SVG too.
|
||||
styleElement = document.createElement("style");
|
||||
}
|
||||
styleElement.setAttribute("id", "stylus-" + styleId);
|
||||
styleElement.setAttribute("class", "stylus");
|
||||
styleElement.setAttribute("type", "text/css");
|
||||
styleElement.appendChild(document.createTextNode(sections.map(function(section) {
|
||||
return section.code;
|
||||
}).join("\n")));
|
||||
addStyleElement(styleElement, document);
|
||||
g_styleElements[styleElement.id] = styleElement;
|
||||
let el = document.getElementById(ID_PREFIX + styleId);
|
||||
if (el) {
|
||||
return;
|
||||
}
|
||||
if (document.documentElement instanceof SVGSVGElement) {
|
||||
// SVG document style
|
||||
el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
|
||||
} else if (document instanceof XMLDocument) {
|
||||
// XML document style
|
||||
el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
|
||||
} else {
|
||||
// HTML document style; also works on HTML-embedded SVG
|
||||
el = document.createElement('style');
|
||||
}
|
||||
Object.assign(el, {
|
||||
id: ID_PREFIX + styleId,
|
||||
className: 'stylus',
|
||||
type: 'text/css',
|
||||
textContent: sections.map(section => section.code).join('\n'),
|
||||
});
|
||||
addStyleElement(el);
|
||||
styleElements.set(el.id, el);
|
||||
disabledElements.delete(styleId);
|
||||
}
|
||||
|
||||
function addStyleElement(styleElement, doc) {
|
||||
if (!doc.documentElement || doc.getElementById(styleElement.id)) {
|
||||
return;
|
||||
}
|
||||
doc.documentElement.appendChild(doc.importNode(styleElement, true))
|
||||
.disabled = g_disableAll;
|
||||
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||
if (iframeIsLoadingSrcDoc(iframe)) {
|
||||
addStyleToIFrameSrcDoc(iframe, styleElement);
|
||||
} else {
|
||||
addStyleElement(styleElement, iframe.contentDocument);
|
||||
}
|
||||
});
|
||||
|
||||
function addStyleElement(el) {
|
||||
if (ROOT && !document.getElementById(el.id)) {
|
||||
ROOT.appendChild(el);
|
||||
el.disabled = disableAll;
|
||||
}
|
||||
}
|
||||
|
||||
function addDocumentStylesToIFrame(iframe) {
|
||||
var doc = iframe.contentDocument;
|
||||
var srcDocIsLoading = iframeIsLoadingSrcDoc(iframe);
|
||||
for (var id in g_styleElements) {
|
||||
if (srcDocIsLoading) {
|
||||
addStyleToIFrameSrcDoc(iframe, g_styleElements[id]);
|
||||
} else {
|
||||
addStyleElement(g_styleElements[id], doc);
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAll(newStyles) {
|
||||
const oldStyles = Array.prototype.slice.call(
|
||||
document.querySelectorAll(`STYLE.stylus[id^="${ID_PREFIX}"]`));
|
||||
oldStyles.forEach(el => (el.id += '-ghost'));
|
||||
styleElements.clear();
|
||||
disabledElements.clear();
|
||||
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
||||
retiredStyleTimers.clear();
|
||||
applyStyles(newStyles);
|
||||
oldStyles.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function addDocumentStylesToAllIFrames() {
|
||||
getDynamicIFrames(document).forEach(addDocumentStylesToIFrame);
|
||||
|
||||
function initDocRewriteObserver() {
|
||||
if (isOwnPage || docRewriteObserver || !styleElements.size) {
|
||||
return;
|
||||
}
|
||||
// re-add styles if we detect documentElement being recreated
|
||||
const reinjectStyles = () => {
|
||||
if (!styleElements) {
|
||||
return orphanCheck && orphanCheck();
|
||||
}
|
||||
ROOT = document.documentElement;
|
||||
for (const el of styleElements.values()) {
|
||||
addStyleElement(document.importNode(el, true));
|
||||
}
|
||||
};
|
||||
// detect documentElement being rewritten from inside the script
|
||||
docRewriteObserver = new MutationObserver(mutations => {
|
||||
for (let m = mutations.length; --m >= 0;) {
|
||||
const added = mutations[m].addedNodes;
|
||||
for (let n = added.length; --n >= 0;) {
|
||||
if (added[n].localName == 'html') {
|
||||
reinjectStyles();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
docRewriteObserver.observe(document, {childList: true});
|
||||
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
|
||||
setTimeout(() => {
|
||||
if (document.documentElement != ROOT) {
|
||||
reinjectStyles();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Only dynamic iframes get the parent document's styles. Other ones should get styles based on their own URLs.
|
||||
function getDynamicIFrames(doc) {
|
||||
return Array.prototype.filter.call(doc.getElementsByTagName('iframe'), iframeIsDynamic);
|
||||
}
|
||||
|
||||
function iframeIsDynamic(f) {
|
||||
var href;
|
||||
try {
|
||||
href = f.contentDocument.location.href;
|
||||
} catch (ex) {
|
||||
// Cross-origin, so it's not a dynamic iframe
|
||||
return false;
|
||||
}
|
||||
return href == document.location.href || href.indexOf("about:") == 0;
|
||||
}
|
||||
|
||||
function iframeIsLoadingSrcDoc(f) {
|
||||
return f.srcdoc && f.contentDocument.all.length <= 3;
|
||||
// 3 nodes or less in total (html, head, body) == new empty iframe about to be overwritten by its 'srcdoc'
|
||||
}
|
||||
|
||||
function addStyleToIFrameSrcDoc(iframe, styleElement) {
|
||||
if (g_disableAll) {
|
||||
return;
|
||||
}
|
||||
iframe.srcdoc += styleElement.outerHTML;
|
||||
// make sure the style is added in case srcdoc was malformed
|
||||
setTimeout(addStyleElement.bind(null, styleElement, iframe.contentDocument), 100);
|
||||
}
|
||||
|
||||
function replaceAll(newStyles, doc, pass2) {
|
||||
var oldStyles = [].slice.call(doc.querySelectorAll("STYLE.stylus" + (pass2 ? "[id$='-ghost']" : "")));
|
||||
if (!pass2) {
|
||||
oldStyles.forEach(function(style) { style.id += "-ghost"; });
|
||||
}
|
||||
getDynamicIFrames(doc).forEach(function(iframe) {
|
||||
replaceAll(newStyles, iframe.contentDocument, pass2);
|
||||
});
|
||||
if (doc == document && !pass2) {
|
||||
g_styleElements = {};
|
||||
applyStyles(newStyles);
|
||||
replaceAll(newStyles, doc, true);
|
||||
}
|
||||
if (pass2) {
|
||||
oldStyles.forEach(function(style) { style.remove(); });
|
||||
}
|
||||
}
|
||||
|
||||
// Observe dynamic IFRAMEs being added
|
||||
function initObserver() {
|
||||
var orphanCheckTimer;
|
||||
|
||||
iframeObserver = new MutationObserver(function(mutations) {
|
||||
clearTimeout(orphanCheckTimer);
|
||||
// MutationObserver runs as a microtask so the timer won't fire until all queued mutations are fired
|
||||
orphanCheckTimer = setTimeout(orphanCheck, 0);
|
||||
|
||||
if (mutations.length > 1000) {
|
||||
// use a much faster method for very complex pages with 100,000 mutations
|
||||
// (observer usually receives 1k-10k mutations per call)
|
||||
addDocumentStylesToAllIFrames();
|
||||
return;
|
||||
}
|
||||
// move the check out of current execution context
|
||||
// because some same-domain (!) iframes fail to load when their "contentDocument" is accessed (!)
|
||||
// namely gmail's old chat iframe talkgadget.google.com
|
||||
setTimeout(process.bind(null, mutations), 0);
|
||||
});
|
||||
|
||||
function process(mutations) {
|
||||
for (var m = 0, ml = mutations.length; m < ml; m++) {
|
||||
var mutation = mutations[m];
|
||||
if (mutation.type === "childList") {
|
||||
for (var n = 0, nodes = mutation.addedNodes, nl = nodes.length; n < nl; n++) {
|
||||
var node = nodes[n];
|
||||
if (node.localName === "iframe" && iframeIsDynamic(node)) {
|
||||
addDocumentStylesToIFrame(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iframeObserver.start = function() {
|
||||
// will be ignored by browser if already observing
|
||||
iframeObserver.observe(document, {childList: true, subtree: true});
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
orphanCheckTimer = 0;
|
||||
var port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach the mutation observer
|
||||
iframeObserver.takeRecords();
|
||||
iframeObserver.disconnect();
|
||||
iframeObserver = null;
|
||||
// we can detach event listeners
|
||||
document.removeEventListener("DOMContentLoaded", onDOMContentLoaded);
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
|
||||
// we can destroy global functions in this context to free up memory
|
||||
[
|
||||
'addDocumentStylesToAllIFrames',
|
||||
'addDocumentStylesToIFrame',
|
||||
'addStyleElement',
|
||||
'addStyleToIFrameSrcDoc',
|
||||
'applyOnMessage',
|
||||
'applySections',
|
||||
'applyStyles',
|
||||
'disableAll',
|
||||
'getDynamicIFrames',
|
||||
'iframeIsDynamic',
|
||||
'iframeIsLoadingSrcDoc',
|
||||
'initObserver',
|
||||
'removeStyle',
|
||||
'replaceAll',
|
||||
'requestStyles',
|
||||
'retireStyle'
|
||||
].forEach(fn => window[fn] = null);
|
||||
|
||||
// we can destroy global variables
|
||||
g_styleElements = iframeObserver = retiredStyleIds = null;
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
const port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach the mutation observer
|
||||
if (docRewriteObserver) {
|
||||
docRewriteObserver.disconnect();
|
||||
}
|
||||
// we can detach event listeners
|
||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy our globals in this context to free up memory
|
||||
[ // functions
|
||||
'addStyleElement',
|
||||
'applyOnMessage',
|
||||
'applySections',
|
||||
'applyStyles',
|
||||
'applyStyleState',
|
||||
'doDisableAll',
|
||||
'initDocRewriteObserver',
|
||||
'orphanCheck',
|
||||
'removeStyle',
|
||||
'replaceAll',
|
||||
'requestStyles',
|
||||
// variables
|
||||
'ROOT',
|
||||
'disabledElements',
|
||||
'retiredStyleTimers',
|
||||
'styleElements',
|
||||
'docRewriteObserver',
|
||||
].forEach(fn => (window[fn] = null));
|
||||
}
|
||||
|
|
482
background.js
|
@ -1,236 +1,282 @@
|
|||
/* globals wildcardAsRegExp, KEEP_CHANNEL_OPEN */
|
||||
/* global dbExec, getStyles, saveStyle */
|
||||
'use strict';
|
||||
|
||||
var frameIdMessageable;
|
||||
runTryCatch(function() {
|
||||
chrome.tabs.sendMessage(0, {}, {frameId: 0}, function() {
|
||||
var clearError = chrome.runtime.lastError;
|
||||
frameIdMessageable = true;
|
||||
});
|
||||
// eslint-disable-next-line no-var
|
||||
var browserCommands, contextMenus;
|
||||
|
||||
// *************************************************************************
|
||||
// preload the DB and report errors
|
||||
dbExec().catch((...args) => {
|
||||
args.forEach(arg => 'message' in arg && console.error(arg.message));
|
||||
});
|
||||
|
||||
// This happens right away, sometimes so fast that the content script isn't even ready. That's
|
||||
// why the content script also asks for this stuff.
|
||||
chrome.webNavigation.onCommitted.addListener(webNavigationListener.bind(this, "styleApply"));
|
||||
// Not supported in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1239349
|
||||
if ("onHistoryStateUpdated" in chrome.webNavigation) {
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(webNavigationListener.bind(this, "styleReplaceAll"));
|
||||
}
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(webNavigationListener.bind(this, null));
|
||||
function webNavigationListener(method, data) {
|
||||
// Until Chrome 41, we can't target a frame with a message
|
||||
// (https://developer.chrome.com/extensions/tabs#method-sendMessage)
|
||||
// so a style affecting a page with an iframe will affect the main page as well.
|
||||
// Skip doing this for frames in pre-41 to prevent page flicker.
|
||||
if (data.frameId != 0 && !frameIdMessageable) {
|
||||
return;
|
||||
}
|
||||
getStyles({matchUrl: data.url, enabled: true, asHash: true}, function(styleHash) {
|
||||
if (method) {
|
||||
chrome.tabs.sendMessage(data.tabId, {method: method, styles: styleHash},
|
||||
frameIdMessageable ? {frameId: data.frameId} : undefined);
|
||||
}
|
||||
if (data.frameId == 0) {
|
||||
updateIcon({id: data.tabId, url: data.url}, styleHash);
|
||||
}
|
||||
});
|
||||
// *************************************************************************
|
||||
// register all listeners
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(data =>
|
||||
webNavigationListener(null, data));
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener(data =>
|
||||
webNavigationListener('styleApply', data));
|
||||
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||
webNavigationListener('styleReplaceAll', data));
|
||||
|
||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
webNavigationListener('styleReplaceAll', data));
|
||||
|
||||
chrome.tabs.onAttached.addListener((tabId, data) => {
|
||||
// When an edit page gets attached or detached, remember its state
|
||||
// so we can do the same to the next one to open.
|
||||
chrome.tabs.get(tabId, tab => {
|
||||
if (tab.url.startsWith(URLS.ownOrigin + 'edit.html')) {
|
||||
chrome.windows.get(tab.windowId, {populate: true}, win => {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
prefs.set('openEditInWindow', win.tabs.length == 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) =>
|
||||
contextMenus[info.menuItemId].click(info, tab));
|
||||
|
||||
if ('commands' in chrome) {
|
||||
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||
}
|
||||
|
||||
// catch direct URL hash modifications not invoked via HTML5 history API
|
||||
var tabUrlHasHash = {};
|
||||
chrome.tabs.onUpdated.addListener(function(tabId, info, tab) {
|
||||
if (info.status == "loading" && info.url) {
|
||||
if (info.url.indexOf('#') > 0) {
|
||||
tabUrlHasHash[tabId] = true;
|
||||
} else if (tabUrlHasHash[tabId]) {
|
||||
delete tabUrlHasHash[tabId];
|
||||
} else {
|
||||
// do nothing since the tab neither had # before nor has # now
|
||||
return;
|
||||
}
|
||||
webNavigationListener("styleReplaceAll", {tabId: tabId, frameId: 0, url: info.url});
|
||||
}
|
||||
});
|
||||
chrome.tabs.onRemoved.addListener(function(tabId, info) {
|
||||
delete tabUrlHasHash[tabId];
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
switch (request.method) {
|
||||
case "getStyles":
|
||||
var styles = getStyles(request, sendResponse);
|
||||
// check if this is a main content frame style enumeration
|
||||
if (request.matchUrl && !request.id
|
||||
&& sender && sender.tab && sender.frameId == 0
|
||||
&& sender.tab.url == request.matchUrl) {
|
||||
updateIcon(sender.tab, styles);
|
||||
}
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
case "saveStyle":
|
||||
saveStyle(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
case "invalidateCache":
|
||||
if (typeof invalidateCache != "undefined") {
|
||||
invalidateCache(false);
|
||||
}
|
||||
break;
|
||||
case "healthCheck":
|
||||
getDatabase(function() { sendResponse(true); }, function() { sendResponse(false); });
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
case "openURL":
|
||||
openURL(request);
|
||||
break;
|
||||
case "styleDisableAll":
|
||||
chrome.contextMenus.update("disableAll", {checked: request.disableAll});
|
||||
break;
|
||||
case "prefChanged":
|
||||
if (request.prefName == "show-badge") {
|
||||
chrome.contextMenus.update("show-badge", {checked: request.value});
|
||||
}
|
||||
else if (request.prefName === 'disableAll') {
|
||||
chrome.contextMenus.update("disableAll", {checked: request.value});
|
||||
}
|
||||
break;
|
||||
case "refreshAllTabs":
|
||||
refreshAllTabs().then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
|
||||
if ("commands" in chrome) {
|
||||
chrome.commands.onCommand.addListener(function(command) {
|
||||
switch (command) {
|
||||
case "openManage":
|
||||
openURL({url: chrome.extension.getURL("manage.html")});
|
||||
break;
|
||||
case "styleDisableAll":
|
||||
disableAllStylesToggle();
|
||||
chrome.contextMenus.update("disableAll", {checked: prefs.get("disableAll")});
|
||||
break;
|
||||
}
|
||||
});
|
||||
// *************************************************************************
|
||||
{
|
||||
const onInstall = ({reason}) => {
|
||||
chrome.runtime.onInstalled.removeListener(onInstall);
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
// Open FAQs page once after installation to guide new users.
|
||||
// Do not display it in development mode.
|
||||
if (reason == 'install' && manifest.update_url) {
|
||||
setTimeout(openURL, 100, {
|
||||
url: `http://add0n.com/stylus.html?version=${manifest.version}&type=install`
|
||||
});
|
||||
}
|
||||
// reset L10N cache on UI language change or update
|
||||
const {browserUIlanguage} = tryJSONparse(localStorage.L10N) || {};
|
||||
const UIlang = chrome.i18n.getUILanguage();
|
||||
if (reason == 'update' || browserUIlanguage != UIlang) {
|
||||
localStorage.L10N = JSON.stringify({
|
||||
browserUIlanguage: UIlang,
|
||||
});
|
||||
}
|
||||
};
|
||||
// bind for 60 seconds max and auto-unbind if it's a normal run
|
||||
chrome.runtime.onInstalled.addListener(onInstall);
|
||||
setTimeout(onInstall, 60e3, {reason: 'unbindme'});
|
||||
}
|
||||
|
||||
// contextMenus API is present in ancient Chrome but it throws an exception
|
||||
// upon encountering the unsupported parameter value "browser_action", so we have to catch it.
|
||||
runTryCatch(function() {
|
||||
chrome.contextMenus.create({
|
||||
id: "show-badge", title: chrome.i18n.getMessage("menuShowBadge"),
|
||||
type: "checkbox", contexts: ["browser_action"], checked: prefs.get("show-badge")
|
||||
}, function() { var clearError = chrome.runtime.lastError });
|
||||
chrome.contextMenus.create({
|
||||
id: "disableAll", title: chrome.i18n.getMessage("disableAllStyles"),
|
||||
type: "checkbox", contexts: ["browser_action"], checked: prefs.get("disableAll")
|
||||
}, function() { var clearError = chrome.runtime.lastError });
|
||||
chrome.contextMenus.create({
|
||||
id: "open-manager", title: chrome.i18n.getMessage("openStylesManager"),
|
||||
type: "normal", contexts: ["browser_action"]
|
||||
}, function() {var clearError = chrome.runtime.lastError});
|
||||
// *************************************************************************
|
||||
// browser commands
|
||||
browserCommands = {
|
||||
openManage() {
|
||||
openURL({url: '/manage.html'});
|
||||
},
|
||||
styleDisableAll(info) {
|
||||
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
|
||||
},
|
||||
};
|
||||
|
||||
// *************************************************************************
|
||||
// context menus
|
||||
contextMenus = Object.assign({
|
||||
'show-badge': {
|
||||
title: 'menuShowBadge',
|
||||
click: info => prefs.set(info.menuItemId, info.checked),
|
||||
},
|
||||
'disableAll': {
|
||||
title: 'disableAllStyles',
|
||||
click: browserCommands.styleDisableAll,
|
||||
},
|
||||
'open-manager': {
|
||||
title: 'openStylesManager',
|
||||
click: browserCommands.openManage,
|
||||
},
|
||||
}, prefs.get('editor.contextDelete') && {
|
||||
'editor.contextDelete': {
|
||||
title: 'editDeleteText',
|
||||
type: 'normal',
|
||||
contexts: ['editable'],
|
||||
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||
click: (info, tab) => {
|
||||
chrome.tabs.sendMessage(tab.id, {method: 'editDeleteText'});
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(function(info, tab) {
|
||||
if (info.menuItemId == "disableAll") {
|
||||
disableAllStylesToggle(info.checked);
|
||||
}
|
||||
else if (info.menuItemId === 'show-badge') {
|
||||
prefs.set(info.menuItemId, info.checked);
|
||||
}
|
||||
else if (info.menuItemId === 'open-manager') {
|
||||
openURL({url: chrome.extension.getURL("manage.html")});
|
||||
}
|
||||
});
|
||||
|
||||
function disableAllStylesToggle(newState) {
|
||||
if (newState === undefined || newState === null) {
|
||||
newState = !prefs.get("disableAll");
|
||||
}
|
||||
prefs.set("disableAll", newState);
|
||||
{
|
||||
const createContextMenus = (ids = Object.keys(contextMenus)) => {
|
||||
for (const id of ids) {
|
||||
const item = Object.assign({id}, contextMenus[id]);
|
||||
const prefValue = prefs.readOnlyValues[id];
|
||||
item.title = chrome.i18n.getMessage(item.title);
|
||||
if (!item.type && typeof prefValue == 'boolean') {
|
||||
item.type = 'checkbox';
|
||||
item.checked = prefValue;
|
||||
}
|
||||
if (!item.contexts) {
|
||||
item.contexts = ['browser_action'];
|
||||
}
|
||||
delete item.click;
|
||||
chrome.contextMenus.create(item, ignoreChromeError);
|
||||
}
|
||||
};
|
||||
createContextMenus();
|
||||
prefs.subscribe((id, checked) => {
|
||||
if (id == 'editor.contextDelete') {
|
||||
if (checked) {
|
||||
createContextMenus([id]);
|
||||
} else {
|
||||
chrome.contextMenus.remove(id, ignoreChromeError);
|
||||
}
|
||||
} else {
|
||||
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
|
||||
}
|
||||
}, Object.keys(contextMenus).filter(key => typeof prefs.readOnlyValues[key] == 'boolean'));
|
||||
}
|
||||
|
||||
// Get the DB so that any first run actions will be performed immediately when the background page loads.
|
||||
getDatabase(function() {}, reportError);
|
||||
// *************************************************************************
|
||||
// [re]inject content scripts
|
||||
{
|
||||
const NTP = 'chrome://newtab/';
|
||||
const PING = {method: 'ping'};
|
||||
const ALL_URLS = '<all_urls>';
|
||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||
// expand * as .*?
|
||||
const wildcardAsRegExp = (s, flags) => new RegExp(
|
||||
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
|
||||
.replace(/\*/g, '.*?'), flags);
|
||||
for (const cs of contentScripts) {
|
||||
cs.matches = cs.matches.map(m => (
|
||||
m == ALL_URLS ? m : wildcardAsRegExp(m)
|
||||
));
|
||||
}
|
||||
|
||||
// When an edit page gets attached or detached, remember its state so we can do the same to the next one to open.
|
||||
var editFullUrl = chrome.extension.getURL("edit.html");
|
||||
chrome.tabs.onAttached.addListener(function(tabId, data) {
|
||||
chrome.tabs.get(tabId, function(tabData) {
|
||||
if (tabData.url.indexOf(editFullUrl) == 0) {
|
||||
chrome.windows.get(tabData.windowId, {populate: true}, function(win) {
|
||||
// If there's only one tab in this window, it's been dragged to new window
|
||||
prefs.set("openEditInWindow", win.tabs.length == 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
const injectCS = (cs, tabId) => {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
file: cs.js[0],
|
||||
runAt: cs.run_at,
|
||||
allFrames: cs.all_frames,
|
||||
matchAboutBlank: cs.match_about_blank,
|
||||
}, ignoreChromeError);
|
||||
};
|
||||
|
||||
function openURL(options) {
|
||||
chrome.tabs.query({currentWindow: true, url: options.url}, function(tabs) {
|
||||
// switch to an existing tab with the requested url
|
||||
if (tabs.length) {
|
||||
chrome.tabs.highlight({windowId: tabs[0].windowId, tabs: tabs[0].index}, function (window) {});
|
||||
} else {
|
||||
delete options.method;
|
||||
getActiveTab(function(tab) {
|
||||
// re-use an active new tab page
|
||||
chrome.tabs[tab.url == "chrome://newtab/" ? "update" : "create"](options);
|
||||
});
|
||||
}
|
||||
});
|
||||
const pingCS = (cs, {id, url}) => {
|
||||
cs.matches.some(match => {
|
||||
if ((match == ALL_URLS || url.match(match))
|
||||
&& (!url.startsWith('chrome') || url == NTP)) {
|
||||
chrome.tabs.sendMessage(id, PING, pong => !pong && injectCS(cs, id));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
chrome.tabs.query({}, tabs =>
|
||||
tabs.forEach(tab =>
|
||||
contentScripts.forEach(cs =>
|
||||
pingCS(cs, tab))));
|
||||
}
|
||||
|
||||
var codeMirrorThemes;
|
||||
getCodeMirrorThemes(function(themes) {
|
||||
codeMirrorThemes = themes;
|
||||
});
|
||||
|
||||
// do not use prefs.get('version', null) as it might not yet be available
|
||||
chrome.storage.local.get('version', prefs => {
|
||||
// Open FAQs page once after installation to guide new users,
|
||||
// https://github.com/schomery/stylish-chrome/issues/22#issuecomment-279936160
|
||||
if (!prefs.version) {
|
||||
// do not display the FAQs page in development mode
|
||||
if ('update_url' in chrome.runtime.getManifest()) {
|
||||
let version = chrome.runtime.getManifest().version;
|
||||
chrome.storage.local.set({
|
||||
version
|
||||
}, () => {
|
||||
window.setTimeout(() => {
|
||||
chrome.tabs.create({
|
||||
url: 'http://add0n.com/stylus.html?version=' + version + '&type=install'
|
||||
});
|
||||
}, 3000);
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
// *************************************************************************
|
||||
|
||||
injectContentScripts();
|
||||
|
||||
function injectContentScripts() {
|
||||
const contentScripts = chrome.app.getDetails().content_scripts;
|
||||
for (let cs of contentScripts) {
|
||||
cs.matches = cs.matches.map(m => m == '<all_urls>' ? m : wildcardAsRegExp(m));
|
||||
}
|
||||
chrome.tabs.query({url: '*://*/*'}, tabs => {
|
||||
for (let tab of tabs) {
|
||||
for (let cs of contentScripts) {
|
||||
for (let m of cs.matches) {
|
||||
if (m == '<all_urls>' || tab.url.match(m)) {
|
||||
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, pong => {
|
||||
if (!pong) {
|
||||
chrome.tabs.executeScript(tab.id, {
|
||||
file: cs.js[0],
|
||||
runAt: cs.run_at,
|
||||
allFrames: cs.all_frames,
|
||||
}, result => chrome.runtime.lastError); // ignore lastError just in case
|
||||
}
|
||||
});
|
||||
// inject the content script just once
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
function webNavigationListener(method, {url, tabId, frameId}) {
|
||||
getStyles({matchUrl: url, enabled: true, asHash: true}).then(styles => {
|
||||
if (method && !url.startsWith('chrome:') && tabId >= 0) {
|
||||
chrome.tabs.sendMessage(tabId, {
|
||||
method,
|
||||
// ping own page so it retrieves the styles directly
|
||||
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
|
||||
}, {
|
||||
frameId
|
||||
});
|
||||
}
|
||||
// main page frame id is 0
|
||||
if (frameId == 0) {
|
||||
updateIcon({id: tabId, url}, styles);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
if (tab.id < 0) {
|
||||
return;
|
||||
}
|
||||
if (styles) {
|
||||
stylesReceived(styles);
|
||||
return;
|
||||
}
|
||||
getTabRealURL(tab)
|
||||
.then(url => getStyles({matchUrl: url, enabled: true, asHash: true}))
|
||||
.then(stylesReceived);
|
||||
|
||||
function stylesReceived(styles) {
|
||||
let numStyles = styles.length;
|
||||
if (numStyles === undefined) {
|
||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||
numStyles = 0;
|
||||
for (const id of Object.keys(styles)) {
|
||||
numStyles += id.match(/^\d+$/) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : numStyles == 0 ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && numStyles ? String(numStyles) : '';
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
path: {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: `images/icon/16${postfix}.png`,
|
||||
32: `images/icon/32${postfix}.png`,
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: `images/icon/19${postfix}.png`,
|
||||
38: `images/icon/38${postfix}.png`,
|
||||
// TODO: add Edge preferred sizes: 20, 25, 30, 40
|
||||
},
|
||||
}, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
return;
|
||||
}
|
||||
// Vivaldi bug workaround: setBadgeText must follow setBadgeBackgroundColor
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
getTab(tab.id).then(() => {
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onRuntimeMessage(request, sender, sendResponse) {
|
||||
switch (request.method) {
|
||||
|
||||
case 'getStyles':
|
||||
getStyles(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'saveStyle':
|
||||
saveStyle(request).then(sendResponse);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'healthCheck':
|
||||
dbExec()
|
||||
.then(() => sendResponse(true))
|
||||
.catch(() => sendResponse(false));
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
|
||||
case 'download':
|
||||
download(request.url)
|
||||
.then(sendResponse)
|
||||
.catch(() => sendResponse(null));
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* globals getStyles, saveStyle, invalidateCache, refreshAllTabs, handleUpdate */
|
||||
/* global messageBox, handleUpdate, applyOnMessage */
|
||||
'use strict';
|
||||
|
||||
var STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
var STYLISH_DUMPFILE_EXTENSION = '.json';
|
||||
var STYLISH_DEFAULT_SAVE_NAME = 'stylus-mm-dd-yyyy' + STYLISH_DUMP_FILE_EXT;
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
|
||||
function importFromFile({fileTypeFilter, file} = {}) {
|
||||
return new Promise(resolve => {
|
||||
|
@ -25,7 +25,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
function readFile() {
|
||||
if (file || fileInput.value !== fileInput.initialValue) {
|
||||
file = file || fileInput.files[0];
|
||||
if (file.size > 100*1000*1000) {
|
||||
if (file.size > 100e6) {
|
||||
console.warn("100MB backup? I don't believe you.");
|
||||
importFromString('').then(resolve);
|
||||
return;
|
||||
|
@ -45,103 +45,312 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function importFromString(jsonString) {
|
||||
const json = runTryCatch(() => Array.from(JSON.parse(jsonString))) || [];
|
||||
const numStyles = json.length;
|
||||
|
||||
if (numStyles) {
|
||||
invalidateCache(true);
|
||||
if (!BG) {
|
||||
onBackgroundReady().then(() => importFromString(jsonString));
|
||||
return;
|
||||
}
|
||||
// create objects in background context
|
||||
const json = BG.tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice != 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStyles = json.length && BG.deepCopy(BG.cachedStyles.list || []);
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
return new Promise(resolve => {
|
||||
proceed();
|
||||
function proceed() {
|
||||
const nextStyle = json.shift();
|
||||
if (nextStyle) {
|
||||
saveStyle(nextStyle, {notify: false}).then(style => {
|
||||
handleUpdate(style);
|
||||
setTimeout(proceed, 0);
|
||||
});
|
||||
} else {
|
||||
refreshAllTabs().then(() => {
|
||||
setTimeout(alert, 100, numStyles + ' styles installed/updated');
|
||||
resolve(numStyles);
|
||||
});
|
||||
let oldDigests;
|
||||
chrome.storage.local.get(null, data => (oldDigests = data));
|
||||
|
||||
const stats = {
|
||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||
metaAndCode: {names: [], ids: [], legend: 'importReportLegendUpdatedBoth'},
|
||||
metaOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedMeta'},
|
||||
codeOnly: {names: [], ids: [], legend: 'importReportLegendUpdatedCode'},
|
||||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let lastRenderTime = performance.now();
|
||||
const renderQueue = [];
|
||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
const info = analyze(item);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return BG.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
});
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
renderQueue.length = 0;
|
||||
done(resolve);
|
||||
}
|
||||
|
||||
function analyze(item) {
|
||||
if (!item || !item.name || !item.name.trim() || typeof item != 'object'
|
||||
|| (item.sections && typeof item.sections.slice != 'function')) {
|
||||
stats.invalid.names.push(`#${index}: ${limitString(item && item.name || '')}`);
|
||||
return;
|
||||
}
|
||||
item.name = item.name.trim();
|
||||
const byId = BG.cachedStyles.byId.get(item.id);
|
||||
const byName = oldStylesByName.get(item.name);
|
||||
const oldStyle = byId && byId.name.trim() == item.name || !byName ? byId : byName;
|
||||
if (oldStyle == byName && byName) {
|
||||
item.id = byName.id;
|
||||
}
|
||||
const oldStyleKeys = oldStyle && Object.keys(oldStyle);
|
||||
const metaEqual = oldStyleKeys &&
|
||||
oldStyleKeys.length == Object.keys(item).length &&
|
||||
oldStyleKeys.every(k => k == 'sections' || oldStyle[k] === item[k]);
|
||||
const codeEqual = oldStyle && BG.styleSectionsEqual(oldStyle, item);
|
||||
if (metaEqual && codeEqual) {
|
||||
stats.unchanged.names.push(oldStyle.name);
|
||||
stats.unchanged.ids.push(oldStyle.id);
|
||||
return;
|
||||
}
|
||||
return {oldStyle, metaEqual, codeEqual};
|
||||
}
|
||||
|
||||
function account({style, info, resolve}) {
|
||||
renderQueue.push(style);
|
||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||
renderQueue.length = 0;
|
||||
lastRenderTime = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
const {oldStyle, metaEqual, codeEqual} = info;
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
stats.added.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!metaEqual && !codeEqual) {
|
||||
stats.metaAndCode.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaAndCode.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
if (!codeEqual) {
|
||||
stats.codeOnly.names.push(style.name);
|
||||
stats.codeOnly.ids.push(style.id);
|
||||
return;
|
||||
}
|
||||
stats.metaOnly.names.push(reportNameChange(oldStyle, style));
|
||||
stats.metaOnly.ids.push(style.id);
|
||||
}
|
||||
|
||||
function done(resolve) {
|
||||
const numChanged = stats.metaAndCode.names.length +
|
||||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && refreshAllTabs()).then(() => {
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => {
|
||||
const {ids, names, legend} = stats[kind];
|
||||
const listItemsWithId = (name, i) =>
|
||||
$element({dataset: {id: ids[i]}, textContent: name});
|
||||
const listItems = name =>
|
||||
$element({textContent: name});
|
||||
const block =
|
||||
$element({tag: 'details', dataset: {id: kind}, appendChild: [
|
||||
$element({tag: 'summary', appendChild:
|
||||
$element({tag: 'b', textContent: names.length + ' ' + t(legend)})
|
||||
}),
|
||||
$element({tag: 'small', appendChild:
|
||||
names.map(ids ? listItemsWithId : listItems)
|
||||
}),
|
||||
]});
|
||||
return block;
|
||||
});
|
||||
scrollTo(0, 0);
|
||||
messageBox({
|
||||
title: t('importReportTitle'),
|
||||
contents: report.length ? report : t('importReportUnchanged'),
|
||||
buttons: [t('confirmOK'), numChanged && t('undo')],
|
||||
onshow: bindClick,
|
||||
}).then(({button, enter, esc}) => {
|
||||
if (button == 1) {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
resolve(numChanged);
|
||||
});
|
||||
}
|
||||
|
||||
function undo() {
|
||||
const oldStylesById = new Map(oldStyles.map(style => [style.id, style]));
|
||||
const newIds = [
|
||||
...stats.metaAndCode.ids,
|
||||
...stats.metaOnly.ids,
|
||||
...stats.codeOnly.ids,
|
||||
...stats.added.ids,
|
||||
];
|
||||
let resolve;
|
||||
index = 0;
|
||||
return new Promise(resolve_ => {
|
||||
resolve = resolve_;
|
||||
undoNextId();
|
||||
}).then(BG.refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmOK')],
|
||||
}));
|
||||
function undoNextId() {
|
||||
if (index == newIds.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const id = newIds[index++];
|
||||
deleteStyleSafe({id, notify: false}).then(id => {
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
oldStyle.styleDigest = oldDigests[BG.DIGEST_KEY_PREFIX + id];
|
||||
saveStyleSafe(Object.assign(oldStyle, SAVE_OPTIONS))
|
||||
.then(undoNextId);
|
||||
} else {
|
||||
undoNextId();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function bindClick(box) {
|
||||
const highlightElement = event => {
|
||||
const styleElement = $('#style-' + event.target.dataset.id);
|
||||
if (styleElement) {
|
||||
scrollElementIntoView(styleElement);
|
||||
animateElement(styleElement, {className: 'highlight'});
|
||||
}
|
||||
};
|
||||
for (const block of $$('details')) {
|
||||
if (block.dataset.id != 'invalid') {
|
||||
block.style.cursor = 'pointer';
|
||||
block.onclick = highlightElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function limitString(s, limit = 100) {
|
||||
return s.length <= limit ? s : s.substr(0, limit) + '...';
|
||||
}
|
||||
|
||||
function reportNameChange(oldStyle, newStyle) {
|
||||
return newStyle.name != oldStyle.name
|
||||
? oldStyle.name + ' —> ' + newStyle.name
|
||||
: oldStyle.name;
|
||||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
return getActiveTab().then(activeTab => new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
getStylesSafe({matchUrl: tab.url, enabled: true, asHash: true}).then(styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.id == activeTab.id) {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
chrome.tabs.sendMessage(tab.id, message);
|
||||
}
|
||||
BG.updateIcon(tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function generateFileName() {
|
||||
var today = new Date();
|
||||
var dd = '0' + today.getDate();
|
||||
var mm = '0' + (today.getMonth() + 1);
|
||||
var yyyy = today.getFullYear();
|
||||
|
||||
dd = dd.substr(-2);
|
||||
mm = mm.substr(-2);
|
||||
|
||||
today = mm + '-' + dd + '-' + yyyy;
|
||||
|
||||
return 'stylus-' + today + STYLISH_DUMPFILE_EXTENSION;
|
||||
}
|
||||
|
||||
document.getElementById('file-all-styles').onclick = () => {
|
||||
getStyles({}, function (styles) {
|
||||
let text = JSON.stringify(styles, null, '\t');
|
||||
let fileName = generateFileName() || STYLISH_DEFAULT_SAVE_NAME;
|
||||
|
||||
let url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||
$('#file-all-styles').onclick = () => {
|
||||
Promise.all([
|
||||
BG.chromeLocal.get(null),
|
||||
getStylesSafe(),
|
||||
]).then(([data, styles]) => {
|
||||
styles = styles.map(style => {
|
||||
const styleDigest = data[BG.DIGEST_KEY_PREFIX + style.id];
|
||||
return styleDigest ? Object.assign({styleDigest}, style) : style;
|
||||
});
|
||||
const text = JSON.stringify(styles, null, '\t');
|
||||
const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
|
||||
return url;
|
||||
// for long URLs; https://github.com/schomery/stylish-chrome/issues/13#issuecomment-284582600
|
||||
fetch(url)
|
||||
}).then(fetch)
|
||||
.then(res => res.blob())
|
||||
.then(blob => {
|
||||
let a = document.createElement('a');
|
||||
a.setAttribute('download', fileName);
|
||||
a.setAttribute('href', URL.createObjectURL(blob));
|
||||
a.dispatchEvent(new MouseEvent('click'));
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
Object.assign(document.createElement('a'), {
|
||||
download: generateFileName(),
|
||||
href: objectURL,
|
||||
type: 'application/json',
|
||||
}).dispatchEvent(new MouseEvent('click'));
|
||||
setTimeout(() => URL.revokeObjectURL(objectURL));
|
||||
});
|
||||
});
|
||||
|
||||
function generateFileName() {
|
||||
const today = new Date();
|
||||
const dd = ('0' + today.getDate()).substr(-2);
|
||||
const mm = ('0' + (today.getMonth() + 1)).substr(-2);
|
||||
const yyyy = today.getFullYear();
|
||||
return `stylus-${mm}-${dd}-${yyyy}${STYLUS_BACKUP_FILE_EXT}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
document.getElementById('unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLISH_DUMPFILE_EXTENSION});
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
const dropTarget = Object.assign(document.body, {
|
||||
ondragover: event => {
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type == 'search' ? 'copy' : 'none';
|
||||
dropTarget.classList.toggle('dropzone', hasFiles);
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(dropTarget.fadeoutTimer);
|
||||
dropTarget.classList.remove('fadeout');
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend: event => {
|
||||
dropTarget.classList.add('fadeout');
|
||||
// transitionend event may not fire if the user switched to another tab so we'll use a timer
|
||||
clearTimeout(dropTarget.fadeoutTimer);
|
||||
dropTarget.fadeoutTimer = setTimeout(() => {
|
||||
dropTarget.classList.remove('dropzone', 'fadeout');
|
||||
}, 250);
|
||||
ondragend(event) {
|
||||
animateElement(this, {className: 'fadeout'}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
this.classList.remove('dropzone');
|
||||
});
|
||||
},
|
||||
ondragleave: event => {
|
||||
ondragleave(event) {
|
||||
// Chrome sets screen coords to 0 on Escape key pressed or mouse out of document bounds
|
||||
if (!event.screenX && !event.screenX) {
|
||||
dropTarget.ondragend();
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop: event => {
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
importFromFile({file: event.dataTransfer.files[0]}).then(() => {
|
||||
dropTarget.classList.remove('dropzone');
|
||||
});
|
||||
} else {
|
||||
dropTarget.ondragend();
|
||||
if ($('#onlyUpdates input').checked) {
|
||||
$('#onlyUpdates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
655
beautify/beautify-css-mod.js
Normal file
|
@ -0,0 +1,655 @@
|
|||
/*jshint curly:true, eqeqeq:true, laxbreak:true, noempty:false */
|
||||
/*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation files
|
||||
(the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
CSS Beautifier
|
||||
---------------
|
||||
|
||||
Written by Harutyun Amirjanyan, (amirjanyan@gmail.com)
|
||||
|
||||
Based on code initially developed by: Einar Lielmanis, <einar@jsbeautifier.org>
|
||||
http://jsbeautifier.org/
|
||||
|
||||
Usage:
|
||||
css_beautify(source_text);
|
||||
css_beautify(source_text, options);
|
||||
|
||||
The options are (default in brackets):
|
||||
indent_size (4) — indentation size,
|
||||
indent_char (space) — character to indent with,
|
||||
preserve_newlines (default false) - whether existing line breaks should be preserved,
|
||||
selector_separator_newline (true) - separate selectors with newline or
|
||||
not (e.g. "a,\nbr" or "a, br")
|
||||
end_with_newline (false) - end with a newline
|
||||
newline_between_rules (true) - add a new line after every css rule
|
||||
space_around_selector_separator (false) - ensure space around selector separators:
|
||||
'>', '+', '~' (e.g. "a>b" -> "a > b")
|
||||
e.g
|
||||
|
||||
css_beautify(css_source_text, {
|
||||
'indent_size': 1,
|
||||
'indent_char': '\t',
|
||||
'selector_separator': ' ',
|
||||
'end_with_newline': false,
|
||||
'newline_between_rules': true,
|
||||
'space_around_selector_separator': true
|
||||
});
|
||||
*/
|
||||
|
||||
// http://www.w3.org/TR/CSS21/syndata.html#tokenization
|
||||
// http://www.w3.org/TR/css3-syntax/
|
||||
|
||||
(function() {
|
||||
|
||||
function mergeOpts(allOptions, targetType) {
|
||||
var finalOpts = {};
|
||||
var name;
|
||||
|
||||
for (name in allOptions) {
|
||||
if (name !== targetType) {
|
||||
finalOpts[name] = allOptions[name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//merge in the per type settings for the targetType
|
||||
if (targetType in allOptions) {
|
||||
for (name in allOptions[targetType]) {
|
||||
finalOpts[name] = allOptions[targetType][name];
|
||||
}
|
||||
}
|
||||
return finalOpts;
|
||||
}
|
||||
|
||||
var lineBreak = /\r\n|[\n\r\u2028\u2029]/;
|
||||
var allLineBreaks = new RegExp(lineBreak.source, 'g');
|
||||
|
||||
function css_beautify(source_text, options) {
|
||||
options = options || {};
|
||||
|
||||
// Allow the setting of language/file-type specific options
|
||||
// with inheritance of overall settings
|
||||
options = mergeOpts(options, 'css');
|
||||
|
||||
source_text = source_text || '';
|
||||
|
||||
var newlinesFromLastWSEat = 0;
|
||||
var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4;
|
||||
var indentCharacter = options.indent_char || ' ';
|
||||
var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines;
|
||||
var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
|
||||
var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
|
||||
var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules;
|
||||
var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator;
|
||||
space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator);
|
||||
var eol = options.eol ? options.eol : 'auto';
|
||||
|
||||
/* STYLUS: hack start */
|
||||
const defaultOption = (opt, defaultValue) => opt === undefined ? defaultValue : opt;
|
||||
var newline_between_properties = defaultOption(options.newline_between_properties, true);
|
||||
var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false);
|
||||
var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true);
|
||||
var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true);
|
||||
var translatePos = (options.translate_positions || [])[0];
|
||||
var translatePosIndex = 0;
|
||||
var translatePosLine = translatePos && translatePos.line;
|
||||
var translatePosCol = translatePos && translatePos.ch;
|
||||
var inputPosLine = 0, inputPosCol = 0;
|
||||
var outputPosLine = 0, outputPosCol = 0;
|
||||
/* STYLUS: hack end */
|
||||
|
||||
if (options.indent_with_tabs) {
|
||||
indentCharacter = '\t';
|
||||
indentSize = 1;
|
||||
}
|
||||
|
||||
if (eol === 'auto') {
|
||||
eol = '\n';
|
||||
if (source_text && lineBreak.test(source_text || '')) {
|
||||
eol = source_text.match(lineBreak)[0];
|
||||
}
|
||||
}
|
||||
|
||||
eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n');
|
||||
|
||||
// HACK: newline parsing inconsistent. This brute force normalizes the input.
|
||||
source_text = source_text.replace(allLineBreaks, '\n');
|
||||
|
||||
// tokenizer
|
||||
var whiteRe = /^\s+$/;
|
||||
|
||||
var pos = -1,
|
||||
ch;
|
||||
var parenLevel = 0;
|
||||
|
||||
function next(resetLine, resetCol) {
|
||||
if (resetLine !== undefined) {
|
||||
inputPosLine = resetLine;
|
||||
inputPosCol = resetCol;
|
||||
if (inputPosCol < 0) {
|
||||
inputPosLine--;
|
||||
inputPosCol = pos - source_text.lastIndexOf('\n', pos);
|
||||
}
|
||||
}
|
||||
ch = source_text.charAt(++pos);
|
||||
if (translatePos) {
|
||||
inputPosCol++;
|
||||
if (ch == '\n') {
|
||||
inputPosLine++;
|
||||
inputPosCol = 0;
|
||||
}
|
||||
if (inputPosLine == translatePosLine && inputPosCol >= translatePosCol
|
||||
|| inputPosLine > translatePosLine) {
|
||||
translatePos.line = outputPosLine - (inputPosLine - translatePosLine);
|
||||
translatePos.ch = outputPosCol - (inputPosCol - translatePosCol);
|
||||
translatePos.ch += translatePos.ch ? 1 : 0;
|
||||
translatePos = options.translate_positions[++translatePosIndex];
|
||||
translatePosLine = translatePos && translatePos.line;
|
||||
translatePosCol = translatePos && translatePos.ch;
|
||||
}
|
||||
}
|
||||
return ch || '';
|
||||
}
|
||||
|
||||
function peek(skipWhitespace) {
|
||||
var result = '';
|
||||
var prev_pos = pos;
|
||||
var prevInputPosLine = inputPosLine;
|
||||
var prevInputPosCol = inputPosCol;
|
||||
if (skipWhitespace) {
|
||||
eatWhitespace();
|
||||
}
|
||||
result = source_text.charAt(pos + 1) || '';
|
||||
pos = prev_pos - 1;
|
||||
next(prevInputPosLine, prevInputPosCol - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
function eatString(endChars) {
|
||||
var start = pos;
|
||||
while (next()) {
|
||||
if (ch === "\\") {
|
||||
next();
|
||||
} else if (endChars.indexOf(ch) !== -1) {
|
||||
break;
|
||||
} else if (ch === "\n") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return source_text.substring(start, pos + 1);
|
||||
}
|
||||
|
||||
function peekString(endChar) {
|
||||
var prev_pos = pos;
|
||||
var prevInputPosLine = inputPosLine;
|
||||
var prevInputPosCol = inputPosCol;
|
||||
var str = eatString(endChar);
|
||||
pos = prev_pos - 1;
|
||||
next(prevInputPosLine, prevInputPosCol - 1);
|
||||
return str;
|
||||
}
|
||||
|
||||
function eatWhitespace(preserve_newlines_local) {
|
||||
var result = 0;
|
||||
while (whiteRe.test(peek())) {
|
||||
next();
|
||||
if (ch === '\n' && preserve_newlines_local && preserve_newlines) {
|
||||
print.newLine(true);
|
||||
result++;
|
||||
}
|
||||
}
|
||||
newlinesFromLastWSEat = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
function skipWhitespace() {
|
||||
var result = '';
|
||||
if (ch && whiteRe.test(ch)) {
|
||||
result = ch;
|
||||
}
|
||||
while (whiteRe.test(next())) {
|
||||
result += ch;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function eatComment(singleLine) {
|
||||
var start = pos;
|
||||
singleLine = peek() === "/";
|
||||
next();
|
||||
while (next()) {
|
||||
if (!singleLine && ch === "*" && peek() === "/") {
|
||||
next();
|
||||
break;
|
||||
} else if (singleLine && ch === "\n") {
|
||||
return source_text.substring(start, pos);
|
||||
}
|
||||
}
|
||||
|
||||
return source_text.substring(start, pos) + ch;
|
||||
}
|
||||
|
||||
|
||||
function lookBack(str) {
|
||||
return source_text.substring(pos - str.length, pos).toLowerCase() ===
|
||||
str;
|
||||
}
|
||||
|
||||
// Nested pseudo-class if we are insideRule
|
||||
// and the next special character found opens
|
||||
// a new block
|
||||
function foundNestedPseudoClass() {
|
||||
var openParen = 0;
|
||||
for (var i = pos + 1; i < source_text.length; i++) {
|
||||
var ch = source_text.charAt(i);
|
||||
if (ch === "{") {
|
||||
return true;
|
||||
} else if (ch === '(') {
|
||||
// pseudoclasses can contain ()
|
||||
openParen += 1;
|
||||
} else if (ch === ')') {
|
||||
if (openParen === 0) {
|
||||
return false;
|
||||
}
|
||||
openParen -= 1;
|
||||
} else if (ch === ";" || ch === "}") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// printer
|
||||
var basebaseIndentString = source_text.match(/^[\t ]*/)[0];
|
||||
var singleIndent = new Array(indentSize + 1).join(indentCharacter);
|
||||
var indentLevel = 0;
|
||||
var nestedLevel = 0;
|
||||
|
||||
function indent() {
|
||||
indentLevel++;
|
||||
basebaseIndentString += singleIndent;
|
||||
}
|
||||
|
||||
function outdent() {
|
||||
indentLevel--;
|
||||
basebaseIndentString = basebaseIndentString.slice(0, -indentSize);
|
||||
}
|
||||
|
||||
var print = {};
|
||||
print["{"] = function(ch) {
|
||||
newline_before_open_brace ? print.newLine() : print.singleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true)) {
|
||||
newline_after_open_brace ? print.newLine() : print.singleSpace();
|
||||
}
|
||||
};
|
||||
print["}"] = function(newline) {
|
||||
if (newline) {
|
||||
newline_before_close_brace ? print.newLine() : (print.trim(), print.singleSpace());
|
||||
}
|
||||
output.push('}');
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
|
||||
print._lastCharWhitespace = function() {
|
||||
return whiteRe.test(output[output.length - 1]);
|
||||
};
|
||||
|
||||
print.newLine = function(keepWhitespace) {
|
||||
if (output.length) {
|
||||
if (!keepWhitespace && output[output.length - 1] !== '\n') {
|
||||
print.trim();
|
||||
} else if (output[output.length - 1] === basebaseIndentString) {
|
||||
output.pop();
|
||||
outputPosCol -= basebaseIndentString.length;
|
||||
}
|
||||
output.push('\n');
|
||||
outputPosLine++;
|
||||
outputPosCol = 0;
|
||||
|
||||
if (basebaseIndentString) {
|
||||
output.push(basebaseIndentString);
|
||||
outputPosCol += basebaseIndentString.length;
|
||||
}
|
||||
}
|
||||
};
|
||||
print.singleSpace = function() {
|
||||
if (output.length && !print._lastCharWhitespace()) {
|
||||
output.push(' ');
|
||||
outputPosCol++;
|
||||
}
|
||||
};
|
||||
|
||||
print.preserveSingleSpace = function() {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
};
|
||||
|
||||
print.trim = function() {
|
||||
while (print._lastCharWhitespace()) {
|
||||
const text = output.pop();
|
||||
if (text.indexOf('\n') >= 0) {
|
||||
outputPosLine -= text.match(/\n/g).length;
|
||||
}
|
||||
}
|
||||
outputPosCol = 0;
|
||||
let i = output.length, token;
|
||||
while (--i >= 0 && (token = output[i]) != '\n') {
|
||||
outputPosCol += token.length;
|
||||
}
|
||||
};
|
||||
|
||||
print.text = function(text) {
|
||||
output.push(text);
|
||||
if (text.indexOf('\n') < 0) {
|
||||
outputPosCol += text.length;
|
||||
} else {
|
||||
outputPosLine += text.match(/\n/g).length;
|
||||
outputPosCol = text.length - text.lastIndexOf('\n') - 1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var output = [];
|
||||
/*_____________________--------------------_____________________*/
|
||||
|
||||
var insideRule = false;
|
||||
var insidePropertyValue = false;
|
||||
var enteringConditionalGroup = false;
|
||||
var top_ch = '';
|
||||
var last_top_ch = '';
|
||||
|
||||
while (true) {
|
||||
var whitespace = skipWhitespace();
|
||||
var isAfterSpace = whitespace !== '';
|
||||
var isAfterNewline = whitespace.indexOf('\n') !== -1;
|
||||
last_top_ch = top_ch;
|
||||
top_ch = ch;
|
||||
|
||||
if (!ch) {
|
||||
break;
|
||||
} else if (ch === '/' && peek() === '*') { /* css comment */
|
||||
var header = indentLevel === 0;
|
||||
|
||||
if (isAfterNewline || header) {
|
||||
print.newLine();
|
||||
}
|
||||
|
||||
print.text(eatComment());
|
||||
print.newLine();
|
||||
if (header) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === '/' && peek() === '/') { // single line comment
|
||||
if (!isAfterNewline && last_top_ch !== '{') {
|
||||
print.trim();
|
||||
}
|
||||
print.singleSpace();
|
||||
print.text(eatComment());
|
||||
print.newLine();
|
||||
} else if (ch === '@') {
|
||||
print.preserveSingleSpace();
|
||||
|
||||
// deal with less propery mixins @{...}
|
||||
if (peek() === '{') {
|
||||
print.text(eatString('}'));
|
||||
} else {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
|
||||
// strip trailing space, if present, for hash property checks
|
||||
var variableOrRule = peekString(": ,;{}()[]/='\"");
|
||||
|
||||
if (variableOrRule.match(/[ :]$/)) {
|
||||
// we have a variable or pseudo-class, add it and insert one space before continuing
|
||||
next();
|
||||
variableOrRule = eatString(": ").replace(/\s$/, '');
|
||||
print.text(variableOrRule);
|
||||
print.singleSpace();
|
||||
}
|
||||
|
||||
variableOrRule = variableOrRule.replace(/\s$/, '');
|
||||
|
||||
// might be a nesting at-rule
|
||||
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
|
||||
nestedLevel += 1;
|
||||
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
|
||||
enteringConditionalGroup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ch === '#' && peek() === '{') {
|
||||
print.preserveSingleSpace();
|
||||
print.text(eatString('}'));
|
||||
} else if (ch === '{') {
|
||||
if (peek(true) === '}') {
|
||||
eatWhitespace();
|
||||
next();
|
||||
print.singleSpace();
|
||||
output.push("{");
|
||||
outputPosCol++;
|
||||
print['}'](false);
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else {
|
||||
indent();
|
||||
print["{"](ch);
|
||||
// when entering conditional groups, only rulesets are allowed
|
||||
if (enteringConditionalGroup) {
|
||||
enteringConditionalGroup = false;
|
||||
insideRule = (indentLevel > nestedLevel);
|
||||
} else {
|
||||
// otherwise, declarations are also allowed
|
||||
insideRule = (indentLevel >= nestedLevel);
|
||||
}
|
||||
}
|
||||
} else if (ch === '}') {
|
||||
outdent();
|
||||
print["}"](true);
|
||||
insideRule = false;
|
||||
insidePropertyValue = false;
|
||||
if (nestedLevel) {
|
||||
nestedLevel--;
|
||||
}
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === ":") {
|
||||
eatWhitespace();
|
||||
if ((insideRule || enteringConditionalGroup) &&
|
||||
!(lookBack("&") || foundNestedPseudoClass()) &&
|
||||
!lookBack("(")) {
|
||||
// 'property: value' delimiter
|
||||
// which could be in a conditional group query
|
||||
output.push(':');
|
||||
outputPosCol++;
|
||||
if (!insidePropertyValue) {
|
||||
insidePropertyValue = true;
|
||||
print.singleSpace();
|
||||
}
|
||||
} else {
|
||||
// sass/less parent reference don't use a space
|
||||
// sass nested pseudo-class don't use a space
|
||||
|
||||
// preserve space before pseudoclasses/pseudoelements, as it means "in any child"
|
||||
if (lookBack(" ") && output[output.length - 1] !== " ") {
|
||||
output.push(" ");
|
||||
outputPosCol++;
|
||||
}
|
||||
if (peek() === ":") {
|
||||
// pseudo-element
|
||||
next();
|
||||
output.push("::");
|
||||
outputPosCol += 2;
|
||||
} else {
|
||||
// pseudo-class
|
||||
output.push(':');
|
||||
outputPosCol++;
|
||||
}
|
||||
}
|
||||
} else if (ch === '"' || ch === '\'') {
|
||||
print.preserveSingleSpace();
|
||||
print.text(eatString(ch));
|
||||
} else if (ch === ';') {
|
||||
insidePropertyValue = false;
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true)) {
|
||||
newline_between_properties ? print.newLine() : print.singleSpace();
|
||||
}
|
||||
} else if (ch === '(') { // may be a url
|
||||
if (lookBack("url")) {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
eatWhitespace();
|
||||
if (next()) {
|
||||
if (ch !== ')' && ch !== '"' && ch !== '\'') {
|
||||
print.text(eatString(')'));
|
||||
} else {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
parenLevel++;
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
eatWhitespace();
|
||||
}
|
||||
} else if (ch === ')') {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
parenLevel--;
|
||||
} else if (ch === ',') {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
|
||||
print.newLine();
|
||||
} else {
|
||||
print.singleSpace();
|
||||
}
|
||||
} else if ((ch === '>' || ch === '+' || ch === '~') &&
|
||||
!insidePropertyValue && parenLevel < 1) {
|
||||
//handle combinator spacing
|
||||
if (space_around_combinator) {
|
||||
print.singleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
print.singleSpace();
|
||||
} else {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
eatWhitespace();
|
||||
// squash extra whitespace
|
||||
if (ch && whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
}
|
||||
} else if (ch === ']') {
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
} else if (ch === '[') {
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
} else if (ch === '=') { // no whitespace before or after
|
||||
eatWhitespace();
|
||||
output.push('=');
|
||||
outputPosCol++;
|
||||
if (whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
} else {
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
outputPosCol++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sweetCode = '';
|
||||
if (basebaseIndentString) {
|
||||
sweetCode += basebaseIndentString;
|
||||
}
|
||||
|
||||
sweetCode += output.join('').replace(/[\r\n\t ]+$/, '');
|
||||
|
||||
// establish end_with_newline
|
||||
if (end_with_newline) {
|
||||
sweetCode += '\n';
|
||||
}
|
||||
|
||||
if (eol !== '\n') {
|
||||
sweetCode = sweetCode.replace(/[\n]/g, eol);
|
||||
}
|
||||
|
||||
return sweetCode;
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule
|
||||
css_beautify.NESTED_AT_RULE = {
|
||||
"@page": true,
|
||||
"@font-face": true,
|
||||
"@keyframes": true,
|
||||
// also in CONDITIONAL_GROUP_RULE below
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
"@document": true
|
||||
};
|
||||
css_beautify.CONDITIONAL_GROUP_RULE = {
|
||||
"@media": true,
|
||||
"@supports": true,
|
||||
"@document": true
|
||||
};
|
||||
|
||||
/*global define */
|
||||
if (typeof define === "function" && define.amd) {
|
||||
// Add support for AMD ( https://github.com/amdjs/amdjs-api/wiki/AMD#defineamd-property- )
|
||||
define([], function() {
|
||||
return {
|
||||
css_beautify: css_beautify
|
||||
};
|
||||
});
|
||||
} else if (typeof exports !== "undefined") {
|
||||
// Add support for CommonJS. Just put this file somewhere on your require.paths
|
||||
// and you will be able to `var html_beautify = require("beautify").html_beautify`.
|
||||
exports.css_beautify = css_beautify;
|
||||
} else if (typeof window !== "undefined") {
|
||||
// If we're running a web page and don't have either of the above, add our one global
|
||||
window.css_beautify = css_beautify;
|
||||
} else if (typeof global !== "undefined") {
|
||||
// If we don't even have window, try global.
|
||||
global.css_beautify = css_beautify;
|
||||
}
|
||||
|
||||
}());
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2007-2013 Einar Lielmanis and contributors.
|
||||
Copyright (c) 2007-2017 Einar Lielmanis, Liam Newman, and contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation files
|
||||
|
@ -39,13 +39,15 @@
|
|||
css_beautify(source_text, options);
|
||||
|
||||
The options are (default in brackets):
|
||||
indent_size (4) — indentation size,
|
||||
indent_char (space) — character to indent with,
|
||||
selector_separator_newline (true) - separate selectors with newline or
|
||||
not (e.g. "a,\nbr" or "a, br")
|
||||
end_with_newline (false) - end with a newline
|
||||
newline_between_rules (true) - add a new line after every css rule
|
||||
|
||||
indent_size (4) — indentation size,
|
||||
indent_char (space) — character to indent with,
|
||||
preserve_newlines (default false) - whether existing line breaks should be preserved,
|
||||
selector_separator_newline (true) - separate selectors with newline or
|
||||
not (e.g. "a,\nbr" or "a, br")
|
||||
end_with_newline (false) - end with a newline
|
||||
newline_between_rules (true) - add a new line after every css rule
|
||||
space_around_selector_separator (false) - ensure space around selector separators:
|
||||
'>', '+', '~' (e.g. "a>b" -> "a > b")
|
||||
e.g
|
||||
|
||||
css_beautify(css_source_text, {
|
||||
|
@ -53,7 +55,8 @@
|
|||
'indent_char': '\t',
|
||||
'selector_separator': ' ',
|
||||
'end_with_newline': false,
|
||||
'newline_between_rules': true
|
||||
'newline_between_rules': true,
|
||||
'space_around_selector_separator': true
|
||||
});
|
||||
*/
|
||||
|
||||
|
@ -61,30 +64,69 @@
|
|||
// http://www.w3.org/TR/css3-syntax/
|
||||
|
||||
(function() {
|
||||
|
||||
function mergeOpts(allOptions, targetType) {
|
||||
var finalOpts = {};
|
||||
var name;
|
||||
|
||||
for (name in allOptions) {
|
||||
if (name !== targetType) {
|
||||
finalOpts[name] = allOptions[name];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//merge in the per type settings for the targetType
|
||||
if (targetType in allOptions) {
|
||||
for (name in allOptions[targetType]) {
|
||||
finalOpts[name] = allOptions[targetType][name];
|
||||
}
|
||||
}
|
||||
return finalOpts;
|
||||
}
|
||||
|
||||
var lineBreak = /\r\n|[\n\r\u2028\u2029]/;
|
||||
var allLineBreaks = new RegExp(lineBreak.source, 'g');
|
||||
|
||||
function css_beautify(source_text, options) {
|
||||
function defaultOption(opt, defaultValue) {
|
||||
return opt === undefined ? defaultValue : opt;
|
||||
}
|
||||
options = options || {};
|
||||
var indentSize = options.indent_size || 4;
|
||||
var indentCharacter = options.indent_char || ' ';
|
||||
var selectorSeparatorNewline = defaultOption(options.selector_separator_newline, true);
|
||||
var end_with_newline = defaultOption(options.end_with_newline, false);
|
||||
var newline_between_rules = defaultOption(options.newline_between_rules, true);
|
||||
var newline_between_properties = defaultOption(options.newline_between_properties, true);
|
||||
var newline_before_open_brace = defaultOption(options.newline_before_open_brace, false);
|
||||
var newline_after_open_brace = defaultOption(options.newline_after_open_brace, true);
|
||||
var newline_before_close_brace = defaultOption(options.newline_before_close_brace, true);
|
||||
|
||||
// compatibility
|
||||
if (typeof indentSize === "string") {
|
||||
indentSize = parseInt(indentSize, 10);
|
||||
// Allow the setting of language/file-type specific options
|
||||
// with inheritance of overall settings
|
||||
options = mergeOpts(options, 'css');
|
||||
|
||||
source_text = source_text || '';
|
||||
|
||||
var newlinesFromLastWSEat = 0;
|
||||
var indentSize = options.indent_size ? parseInt(options.indent_size, 10) : 4;
|
||||
var indentCharacter = options.indent_char || ' ';
|
||||
var preserve_newlines = (options.preserve_newlines === undefined) ? false : options.preserve_newlines;
|
||||
var selectorSeparatorNewline = (options.selector_separator_newline === undefined) ? true : options.selector_separator_newline;
|
||||
var end_with_newline = (options.end_with_newline === undefined) ? false : options.end_with_newline;
|
||||
var newline_between_rules = (options.newline_between_rules === undefined) ? true : options.newline_between_rules;
|
||||
var space_around_combinator = (options.space_around_combinator === undefined) ? false : options.space_around_combinator;
|
||||
space_around_combinator = space_around_combinator || ((options.space_around_selector_separator === undefined) ? false : options.space_around_selector_separator);
|
||||
var eol = options.eol ? options.eol : 'auto';
|
||||
|
||||
if (options.indent_with_tabs) {
|
||||
indentCharacter = '\t';
|
||||
indentSize = 1;
|
||||
}
|
||||
|
||||
if (eol === 'auto') {
|
||||
eol = '\n';
|
||||
if (source_text && lineBreak.test(source_text || '')) {
|
||||
eol = source_text.match(lineBreak)[0];
|
||||
}
|
||||
}
|
||||
|
||||
eol = eol.replace(/\\r/, '\r').replace(/\\n/, '\n');
|
||||
|
||||
// HACK: newline parsing inconsistent. This brute force normalizes the input.
|
||||
source_text = source_text.replace(allLineBreaks, '\n');
|
||||
|
||||
// tokenizer
|
||||
var whiteRe = /^\s+$/;
|
||||
var wordRe = /[\w$\-_]/;
|
||||
|
||||
var pos = -1,
|
||||
ch;
|
||||
|
@ -96,6 +138,7 @@
|
|||
}
|
||||
|
||||
function peek(skipWhitespace) {
|
||||
var result = '';
|
||||
var prev_pos = pos;
|
||||
if (skipWhitespace) {
|
||||
eatWhitespace();
|
||||
|
@ -128,12 +171,16 @@
|
|||
return str;
|
||||
}
|
||||
|
||||
function eatWhitespace() {
|
||||
var result = '';
|
||||
function eatWhitespace(preserve_newlines_local) {
|
||||
var result = 0;
|
||||
while (whiteRe.test(peek())) {
|
||||
next();
|
||||
result += ch;
|
||||
if (ch === '\n' && preserve_newlines_local && preserve_newlines) {
|
||||
print.newLine(true);
|
||||
result++;
|
||||
}
|
||||
}
|
||||
newlinesFromLastWSEat = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -174,11 +221,20 @@
|
|||
// and the next special character found opens
|
||||
// a new block
|
||||
function foundNestedPseudoClass() {
|
||||
var openParen = 0;
|
||||
for (var i = pos + 1; i < source_text.length; i++) {
|
||||
var ch = source_text.charAt(i);
|
||||
if (ch === "{") {
|
||||
return true;
|
||||
} else if (ch === ";" || ch === "}" || ch === ")") {
|
||||
} else if (ch === '(') {
|
||||
// pseudoclasses can contain ()
|
||||
openParen += 1;
|
||||
} else if (ch === ')') {
|
||||
if (openParen === 0) {
|
||||
return false;
|
||||
}
|
||||
openParen -= 1;
|
||||
} else if (ch === ";" || ch === "}") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -203,14 +259,20 @@
|
|||
|
||||
var print = {};
|
||||
print["{"] = function(ch) {
|
||||
newline_before_open_brace ? output.push('\n') : print.singleSpace();
|
||||
print.singleSpace();
|
||||
output.push(ch);
|
||||
newline_after_open_brace ? print.newLine() : print.singleSpace();
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
print["}"] = function(ch) {
|
||||
newline_before_close_brace ? print.newLine() : print.singleSpace();
|
||||
output.push(ch);
|
||||
print.newLine();
|
||||
print["}"] = function(newline) {
|
||||
if (newline) {
|
||||
print.newLine();
|
||||
}
|
||||
output.push('}');
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
};
|
||||
|
||||
print._lastCharWhitespace = function() {
|
||||
|
@ -218,15 +280,17 @@
|
|||
};
|
||||
|
||||
print.newLine = function(keepWhitespace) {
|
||||
if (!keepWhitespace) {
|
||||
print.trim();
|
||||
}
|
||||
|
||||
if (output.length) {
|
||||
if (!keepWhitespace && output[output.length - 1] !== '\n') {
|
||||
print.trim();
|
||||
} else if (output[output.length - 1] === basebaseIndentString) {
|
||||
output.pop();
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
if (basebaseIndentString) {
|
||||
output.push(basebaseIndentString);
|
||||
|
||||
if (basebaseIndentString) {
|
||||
output.push(basebaseIndentString);
|
||||
}
|
||||
}
|
||||
};
|
||||
print.singleSpace = function() {
|
||||
|
@ -235,6 +299,12 @@
|
|||
}
|
||||
};
|
||||
|
||||
print.preserveSingleSpace = function() {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
};
|
||||
|
||||
print.trim = function() {
|
||||
while (print._lastCharWhitespace()) {
|
||||
output.pop();
|
||||
|
@ -243,12 +313,10 @@
|
|||
|
||||
|
||||
var output = [];
|
||||
if (basebaseIndentString) {
|
||||
output.push(basebaseIndentString);
|
||||
}
|
||||
/*_____________________--------------------_____________________*/
|
||||
|
||||
var insideRule = false;
|
||||
var insidePropertyValue = false;
|
||||
var enteringConditionalGroup = false;
|
||||
var top_ch = '';
|
||||
var last_top_ch = '';
|
||||
|
@ -263,8 +331,12 @@
|
|||
if (!ch) {
|
||||
break;
|
||||
} else if (ch === '/' && peek() === '*') { /* css comment */
|
||||
var header = lookBack("");
|
||||
print.newLine();
|
||||
var header = indentLevel === 0;
|
||||
|
||||
if (isAfterNewline || header) {
|
||||
print.newLine();
|
||||
}
|
||||
|
||||
output.push(eatComment());
|
||||
print.newLine();
|
||||
if (header) {
|
||||
|
@ -278,40 +350,46 @@
|
|||
output.push(eatComment());
|
||||
print.newLine();
|
||||
} else if (ch === '@') {
|
||||
// pass along the space we found as a separate item
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
output.push(ch);
|
||||
print.preserveSingleSpace();
|
||||
|
||||
// strip trailing space, if present, for hash property checks
|
||||
var variableOrRule = peekString(": ,;{}()[]/='\"");
|
||||
// deal with less propery mixins @{...}
|
||||
if (peek() === '{') {
|
||||
output.push(eatString('}'));
|
||||
} else {
|
||||
output.push(ch);
|
||||
|
||||
if (variableOrRule.match(/[ :]$/)) {
|
||||
// we have a variable or pseudo-class, add it and insert one space before continuing
|
||||
next();
|
||||
variableOrRule = eatString(": ").replace(/\s$/, '');
|
||||
output.push(variableOrRule);
|
||||
print.singleSpace();
|
||||
}
|
||||
// strip trailing space, if present, for hash property checks
|
||||
var variableOrRule = peekString(": ,;{}()[]/='\"");
|
||||
|
||||
variableOrRule = variableOrRule.replace(/\s$/, '')
|
||||
if (variableOrRule.match(/[ :]$/)) {
|
||||
// we have a variable or pseudo-class, add it and insert one space before continuing
|
||||
next();
|
||||
variableOrRule = eatString(": ").replace(/\s$/, '');
|
||||
output.push(variableOrRule);
|
||||
print.singleSpace();
|
||||
}
|
||||
|
||||
// might be a nesting at-rule
|
||||
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
|
||||
nestedLevel += 1;
|
||||
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
|
||||
enteringConditionalGroup = true;
|
||||
variableOrRule = variableOrRule.replace(/\s$/, '');
|
||||
|
||||
// might be a nesting at-rule
|
||||
if (variableOrRule in css_beautify.NESTED_AT_RULE) {
|
||||
nestedLevel += 1;
|
||||
if (variableOrRule in css_beautify.CONDITIONAL_GROUP_RULE) {
|
||||
enteringConditionalGroup = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ch === '#' && peek() === '{') {
|
||||
print.preserveSingleSpace();
|
||||
output.push(eatString('}'));
|
||||
} else if (ch === '{') {
|
||||
if (peek(true) === '}') {
|
||||
eatWhitespace();
|
||||
next();
|
||||
print.singleSpace();
|
||||
output.push("{}");
|
||||
print.newLine();
|
||||
if (newline_between_rules && indentLevel === 0) {
|
||||
output.push("{");
|
||||
print['}'](false);
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else {
|
||||
|
@ -328,25 +406,35 @@
|
|||
}
|
||||
} else if (ch === '}') {
|
||||
outdent();
|
||||
print["}"](ch);
|
||||
print["}"](true);
|
||||
insideRule = false;
|
||||
insidePropertyValue = false;
|
||||
if (nestedLevel) {
|
||||
nestedLevel--;
|
||||
}
|
||||
if (newline_between_rules && indentLevel === 0) {
|
||||
if (newlinesFromLastWSEat < 2 && newline_between_rules && indentLevel === 0) {
|
||||
print.newLine(true);
|
||||
}
|
||||
} else if (ch === ":") {
|
||||
eatWhitespace();
|
||||
if ((insideRule || enteringConditionalGroup) &&
|
||||
!(lookBack("&") || foundNestedPseudoClass())) {
|
||||
!(lookBack("&") || foundNestedPseudoClass()) &&
|
||||
!lookBack("(")) {
|
||||
// 'property: value' delimiter
|
||||
// which could be in a conditional group query
|
||||
output.push(':');
|
||||
print.singleSpace();
|
||||
if (!insidePropertyValue) {
|
||||
insidePropertyValue = true;
|
||||
print.singleSpace();
|
||||
}
|
||||
} else {
|
||||
// sass/less parent reference don't use a space
|
||||
// sass nested pseudo-class don't use a space
|
||||
|
||||
// preserve space before pseudoclasses/pseudoelements, as it means "in any child"
|
||||
if (lookBack(" ") && output[output.length - 1] !== " ") {
|
||||
output.push(" ");
|
||||
}
|
||||
if (peek() === ":") {
|
||||
// pseudo-element
|
||||
next();
|
||||
|
@ -357,13 +445,14 @@
|
|||
}
|
||||
}
|
||||
} else if (ch === '"' || ch === '\'') {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
print.preserveSingleSpace();
|
||||
output.push(eatString(ch));
|
||||
} else if (ch === ';') {
|
||||
insidePropertyValue = false;
|
||||
output.push(ch);
|
||||
newline_between_properties ? print.newLine() : print.singleSpace();
|
||||
if (!eatWhitespace(true)) {
|
||||
print.newLine();
|
||||
}
|
||||
} else if (ch === '(') { // may be a url
|
||||
if (lookBack("url")) {
|
||||
output.push(ch);
|
||||
|
@ -377,9 +466,7 @@
|
|||
}
|
||||
} else {
|
||||
parenLevel++;
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
}
|
||||
|
@ -388,38 +475,58 @@
|
|||
parenLevel--;
|
||||
} else if (ch === ',') {
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
if (!insideRule && selectorSeparatorNewline && parenLevel < 1) {
|
||||
if (!eatWhitespace(true) && selectorSeparatorNewline && !insidePropertyValue && parenLevel < 1) {
|
||||
print.newLine();
|
||||
} else {
|
||||
print.singleSpace();
|
||||
}
|
||||
} else if ((ch === '>' || ch === '+' || ch === '~') &&
|
||||
!insidePropertyValue && parenLevel < 1) {
|
||||
//handle combinator spacing
|
||||
if (space_around_combinator) {
|
||||
print.singleSpace();
|
||||
output.push(ch);
|
||||
print.singleSpace();
|
||||
} else {
|
||||
output.push(ch);
|
||||
eatWhitespace();
|
||||
// squash extra whitespace
|
||||
if (ch && whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
}
|
||||
} else if (ch === ']') {
|
||||
output.push(ch);
|
||||
} else if (ch === '[') {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
}
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
} else if (ch === '=') { // no whitespace before or after
|
||||
eatWhitespace()
|
||||
ch = '=';
|
||||
output.push(ch);
|
||||
} else {
|
||||
if (isAfterSpace) {
|
||||
print.singleSpace();
|
||||
eatWhitespace();
|
||||
output.push('=');
|
||||
if (whiteRe.test(ch)) {
|
||||
ch = '';
|
||||
}
|
||||
|
||||
} else {
|
||||
print.preserveSingleSpace();
|
||||
output.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sweetCode = output.join('').replace(/[\r\n\t ]+$/, '');
|
||||
var sweetCode = '';
|
||||
if (basebaseIndentString) {
|
||||
sweetCode += basebaseIndentString;
|
||||
}
|
||||
|
||||
sweetCode += output.join('').replace(/[\r\n\t ]+$/, '');
|
||||
|
||||
// establish end_with_newline
|
||||
if (end_with_newline) {
|
||||
sweetCode += "\n";
|
||||
sweetCode += '\n';
|
||||
}
|
||||
|
||||
if (eol !== '\n') {
|
||||
sweetCode = sweetCode.replace(/[\n]/g, eol);
|
||||
}
|
||||
|
||||
return sweetCode;
|
||||
|
|
198
codemirror-overwrites/addon/search/match-highlighter.js
Normal file
|
@ -0,0 +1,198 @@
|
|||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Highlighting text that matches the selection
|
||||
//
|
||||
// Defines an option highlightSelectionMatches, which, when enabled,
|
||||
// will style strings that match the selection throughout the
|
||||
// document.
|
||||
//
|
||||
// The option can be set to true to simply enable it, or to a
|
||||
// {minChars, style, wordsOnly, showToken, delay} object to explicitly
|
||||
// configure it. minChars is the minimum amount of characters that should be
|
||||
// selected for the behavior to occur, and style is the token style to
|
||||
// apply to the matches. This will be prefixed by "cm-" to create an
|
||||
// actual CSS class name. If wordsOnly is enabled, the matches will be
|
||||
// highlighted only if the selected text is a word. showToken, when enabled,
|
||||
// will cause the current token to be highlighted when nothing is selected.
|
||||
// delay is used to specify how much time to wait, in milliseconds, before
|
||||
// highlighting the matches. If annotateScrollbar is enabled, the occurences
|
||||
// will be highlighted on the scrollbar via the matchesonscrollbar addon.
|
||||
|
||||
/* STYLUS: hack start (part 1) */
|
||||
/* eslint curly: 1, brace-style:1, strict: 0, quotes: 0, semi: 1, indent: 1 */
|
||||
/* eslint no-var: 0, block-scoped-var: 0, no-redeclare: 0, no-unused-expressions: 1 */
|
||||
/* global CodeMirror, require, define */
|
||||
/* STYLUS: hack end (part 1) */
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./matchesonscrollbar"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "./matchesonscrollbar"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var defaults = {
|
||||
style: "matchhighlight",
|
||||
minChars: 2,
|
||||
delay: 100,
|
||||
wordsOnly: false,
|
||||
annotateScrollbar: false,
|
||||
showToken: false,
|
||||
trim: true
|
||||
}
|
||||
|
||||
function State(options) {
|
||||
this.options = {}
|
||||
for (var name in defaults)
|
||||
this.options[name] = (options && options.hasOwnProperty(name) ? options : defaults)[name]
|
||||
this.overlay = this.timeout = null;
|
||||
this.matchesonscroll = null;
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
removeOverlay(cm);
|
||||
clearTimeout(cm.state.matchHighlighter.timeout);
|
||||
cm.state.matchHighlighter = null;
|
||||
cm.off("cursorActivity", cursorActivity);
|
||||
cm.off("focus", onFocus)
|
||||
}
|
||||
if (val) {
|
||||
var state = cm.state.matchHighlighter = new State(val);
|
||||
if (cm.hasFocus()) {
|
||||
state.active = true
|
||||
highlightMatches(cm)
|
||||
} else {
|
||||
cm.on("focus", onFocus)
|
||||
}
|
||||
cm.on("cursorActivity", cursorActivity);
|
||||
}
|
||||
});
|
||||
|
||||
function cursorActivity(cm) {
|
||||
var state = cm.state.matchHighlighter;
|
||||
if (state.active || cm.hasFocus()) scheduleHighlight(cm, state)
|
||||
}
|
||||
|
||||
function onFocus(cm) {
|
||||
var state = cm.state.matchHighlighter
|
||||
if (!state.active) {
|
||||
state.active = true
|
||||
scheduleHighlight(cm, state)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleHighlight(cm, state) {
|
||||
clearTimeout(state.timeout);
|
||||
state.timeout = setTimeout(function() {highlightMatches(cm);}, state.options.delay);
|
||||
}
|
||||
|
||||
function addOverlay(cm, query, hasBoundary, style) {
|
||||
var state = cm.state.matchHighlighter;
|
||||
/* STYLUS: hack start (part 2) */
|
||||
cm.addOverlay(state.overlay = makeOverlay(cm, query, hasBoundary, style));
|
||||
/* STYLUS: hack end (part 2) */
|
||||
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
||||
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
|
||||
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
||||
{className: "CodeMirror-selection-highlight-scrollbar"});
|
||||
}
|
||||
}
|
||||
|
||||
function removeOverlay(cm) {
|
||||
var state = cm.state.matchHighlighter;
|
||||
if (state.overlay) {
|
||||
cm.removeOverlay(state.overlay);
|
||||
state.overlay = null;
|
||||
if (state.matchesonscroll) {
|
||||
state.matchesonscroll.clear();
|
||||
state.matchesonscroll = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function highlightMatches(cm) {
|
||||
cm.operation(function() {
|
||||
var state = cm.state.matchHighlighter;
|
||||
if (!cm.somethingSelected() && state.options.showToken) {
|
||||
var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
|
||||
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
|
||||
while (start && re.test(line.charAt(start - 1))) --start;
|
||||
while (end < line.length && re.test(line.charAt(end))) ++end;
|
||||
/* STYLUS: hack start */
|
||||
const token = line.slice(start, end);
|
||||
if (token !== state.lastToken) {
|
||||
state.lastToken = token;
|
||||
removeOverlay(cm);
|
||||
if (token) {
|
||||
addOverlay(cm, token, re, state.options.style);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
removeOverlay(cm);
|
||||
/* STYLUS: hack end */
|
||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||
if (from.line != to.line) return;
|
||||
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
|
||||
var selection = cm.getRange(from, to)
|
||||
if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
|
||||
if (selection.length >= state.options.minChars)
|
||||
addOverlay(cm, selection, false, state.options.style);
|
||||
});
|
||||
}
|
||||
|
||||
function isWord(cm, from, to) {
|
||||
var str = cm.getRange(from, to);
|
||||
if (str.match(/^\w+$/) !== null) {
|
||||
if (from.ch > 0) {
|
||||
var pos = {line: from.line, ch: from.ch - 1};
|
||||
var chr = cm.getRange(pos, from);
|
||||
if (chr.match(/\W/) === null) return false;
|
||||
}
|
||||
if (to.ch < cm.getLine(from.line).length) {
|
||||
var pos = {line: to.line, ch: to.ch + 1};
|
||||
var chr = cm.getRange(to, pos);
|
||||
if (chr.match(/\W/) === null) return false;
|
||||
}
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
function boundariesAround(stream, re) {
|
||||
return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
|
||||
(stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
|
||||
}
|
||||
|
||||
function makeOverlay(cm, query, hasBoundary, style) {
|
||||
/* STYLUS: hack start (part 3) */
|
||||
const approvedClassName = `cm-${style}-approved`;
|
||||
let timer;
|
||||
let occurrences = 0;
|
||||
return {token: function(stream) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
occurrences = 0;
|
||||
timer = null;
|
||||
});
|
||||
if (stream.match(query) &&
|
||||
(!hasBoundary || boundariesAround(stream, hasBoundary))) {
|
||||
occurrences++;
|
||||
if (occurrences == 1) {
|
||||
cm.display.wrapper.classList.remove(approvedClassName);
|
||||
} else if (occurrences == 2) {
|
||||
cm.display.wrapper.classList.add(approvedClassName);
|
||||
}
|
||||
return style;
|
||||
}
|
||||
/* STYLUS: hack end (part 3) */
|
||||
stream.next();
|
||||
stream.skipTo(query.charAt(0)) || stream.skipToEnd();
|
||||
}};
|
||||
}
|
||||
});
|
|
@ -1,3 +1,5 @@
|
|||
MIT License
|
||||
|
||||
Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
|
|
19
codemirror/addon/comment/comment.js
vendored
|
@ -46,12 +46,17 @@
|
|||
|
||||
// Rough heuristic to try and detect lines that are part of multi-line string
|
||||
function probablyInsideString(cm, pos, line) {
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"\`]/.test(line)
|
||||
}
|
||||
|
||||
function getMode(cm, pos) {
|
||||
var mode = cm.getMode()
|
||||
return mode.useInnerComments === false || !mode.innerMode ? mode : cm.getModeAt(pos)
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var self = this, mode = getMode(self, from);
|
||||
var firstLine = self.getLine(from.line);
|
||||
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
|
||||
|
||||
|
@ -95,7 +100,7 @@
|
|||
|
||||
CodeMirror.defineExtension("blockComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var self = this, mode = getMode(self, from);
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) {
|
||||
|
@ -129,7 +134,7 @@
|
|||
|
||||
CodeMirror.defineExtension("uncomment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var self = this, mode = getMode(self, from);
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
|
||||
|
||||
// Try finding line comments
|
||||
|
@ -171,9 +176,11 @@
|
|||
endLine = self.getLine(--end);
|
||||
close = endLine.indexOf(endString);
|
||||
}
|
||||
var insideStart = Pos(start, open + 1), insideEnd = Pos(end, close + 1)
|
||||
if (close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
|
||||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
|
||||
!/comment/.test(self.getTokenTypeAt(insideStart)) ||
|
||||
!/comment/.test(self.getTokenTypeAt(insideEnd)) ||
|
||||
self.getRange(insideStart, insideEnd, "\n").indexOf(endString) > -1)
|
||||
return false;
|
||||
|
||||
// Avoid killing block comments completely outside the selection.
|
||||
|
|
6
codemirror/addon/lint/lint.js
vendored
|
@ -140,7 +140,11 @@
|
|||
if (options.async || getAnnotations.async) {
|
||||
lintAsync(cm, getAnnotations, passOptions)
|
||||
} else {
|
||||
updateLinting(cm, getAnnotations(cm.getValue(), passOptions, cm));
|
||||
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
|
||||
if (annotations.then) annotations.then(function(issues) {
|
||||
updateLinting(cm, issues);
|
||||
});
|
||||
else updateLinting(cm, annotations);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
6
codemirror/addon/scroll/annotatescrollbar.js
vendored
|
@ -77,17 +77,21 @@
|
|||
curLine = pos.line;
|
||||
curLineObj = cm.getLineHandle(curLine);
|
||||
}
|
||||
if (wrapping && curLineObj.height > singleLineH)
|
||||
if ((curLineObj.widgets && curLineObj.widgets.length) ||
|
||||
(wrapping && curLineObj.height > singleLineH))
|
||||
return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
|
||||
var topY = cm.heightAtLine(curLineObj, "local");
|
||||
return topY + (top ? 0 : curLineObj.height);
|
||||
}
|
||||
|
||||
var lastLine = cm.lastLine()
|
||||
if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
|
||||
var ann = anns[i];
|
||||
if (ann.to.line > lastLine) continue;
|
||||
var top = nextTop || getY(ann.from, true) * hScale;
|
||||
var bottom = getY(ann.to, false) * hScale;
|
||||
while (i < anns.length - 1) {
|
||||
if (anns[i + 1].to.line > lastLine) break;
|
||||
nextTop = getY(anns[i + 1].from, true) * hScale;
|
||||
if (nextTop > bottom + .9) break;
|
||||
ann = anns[++i];
|
||||
|
|
4
codemirror/keymap/emacs.js
vendored
|
@ -371,7 +371,9 @@
|
|||
"Shift-Alt-,": "goDocStart", "Shift-Alt-.": "goDocEnd",
|
||||
"Ctrl-S": "findNext", "Ctrl-R": "findPrev", "Ctrl-G": quit, "Shift-Alt-5": "replace",
|
||||
"Alt-/": "autocomplete",
|
||||
"Ctrl-J": "newlineAndIndent", "Enter": false, "Tab": "indentAuto",
|
||||
"Enter": "newlineAndIndent",
|
||||
"Ctrl-J": repeated(function(cm) { cm.replaceSelection("\n", "end"); }),
|
||||
"Tab": "indentAuto",
|
||||
|
||||
"Alt-G G": function(cm) {
|
||||
var prefix = getPrefix(cm, true);
|
||||
|
|
17
codemirror/keymap/sublime.js
vendored
|
@ -152,18 +152,25 @@
|
|||
var text = cm.getRange(from, to);
|
||||
var query = fullWord ? new RegExp("\\b" + text + "\\b") : text;
|
||||
var cur = cm.getSearchCursor(query, to);
|
||||
if (cur.findNext()) {
|
||||
cm.addSelection(cur.from(), cur.to());
|
||||
} else {
|
||||
var found = cur.findNext();
|
||||
if (!found) {
|
||||
cur = cm.getSearchCursor(query, Pos(cm.firstLine(), 0));
|
||||
if (cur.findNext())
|
||||
cm.addSelection(cur.from(), cur.to());
|
||||
found = cur.findNext();
|
||||
}
|
||||
if (!found || isSelectedRange(cm.listSelections(), cur.from(), cur.to()))
|
||||
return CodeMirror.Pass
|
||||
cm.addSelection(cur.from(), cur.to());
|
||||
}
|
||||
if (fullWord)
|
||||
cm.state.sublimeFindFullWord = cm.doc.sel;
|
||||
};
|
||||
|
||||
function isSelectedRange(ranges, from, to) {
|
||||
for (var i = 0; i < ranges.length; i++)
|
||||
if (ranges[i].from() == from && ranges[i].to() == to) return true
|
||||
return false
|
||||
}
|
||||
|
||||
var mirror = "(){}[]";
|
||||
function selectBetweenBrackets(cm) {
|
||||
var ranges = cm.listSelections(), newRanges = []
|
||||
|
|
93
codemirror/keymap/vim.js
vendored
|
@ -142,7 +142,7 @@
|
|||
{ keys: 'X', type: 'operatorMotion', operator: 'delete', motion: 'moveByCharacters', motionArgs: { forward: false }, operatorMotionArgs: { visualLine: true }},
|
||||
{ keys: 'D', type: 'operatorMotion', operator: 'delete', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
|
||||
{ keys: 'D', type: 'operator', operator: 'delete', operatorArgs: { linewise: true }, context: 'visual'},
|
||||
{ keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
|
||||
{ keys: 'Y', type: 'operatorMotion', operator: 'yank', motion: 'expandToLine', motionArgs: { linewise: true }, context: 'normal'},
|
||||
{ keys: 'Y', type: 'operator', operator: 'yank', operatorArgs: { linewise: true }, context: 'visual'},
|
||||
{ keys: 'C', type: 'operatorMotion', operator: 'change', motion: 'moveToEol', motionArgs: { inclusive: true }, context: 'normal'},
|
||||
{ keys: 'C', type: 'operator', operator: 'change', operatorArgs: { linewise: true }, context: 'visual'},
|
||||
|
@ -1245,11 +1245,13 @@
|
|||
}
|
||||
}
|
||||
function onPromptKeyUp(e, query, close) {
|
||||
var keyName = CodeMirror.keyName(e), up;
|
||||
var keyName = CodeMirror.keyName(e), up, offset;
|
||||
if (keyName == 'Up' || keyName == 'Down') {
|
||||
up = keyName == 'Up' ? true : false;
|
||||
offset = e.target ? e.target.selectionEnd : 0;
|
||||
query = vimGlobalState.searchHistoryController.nextMatch(query, up) || '';
|
||||
close(query);
|
||||
if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
|
||||
} else {
|
||||
if ( keyName != 'Left' && keyName != 'Right' && keyName != 'Ctrl' && keyName != 'Alt' && keyName != 'Shift')
|
||||
vimGlobalState.searchHistoryController.reset();
|
||||
|
@ -1281,6 +1283,8 @@
|
|||
clearInputState(cm);
|
||||
close();
|
||||
cm.focus();
|
||||
} else if (keyName == 'Up' || keyName == 'Down') {
|
||||
CodeMirror.e_stop(e);
|
||||
} else if (keyName == 'Ctrl-U') {
|
||||
// Ctrl-U clears input.
|
||||
CodeMirror.e_stop(e);
|
||||
|
@ -1344,7 +1348,7 @@
|
|||
exCommandDispatcher.processCommand(cm, input);
|
||||
}
|
||||
function onPromptKeyDown(e, input, close) {
|
||||
var keyName = CodeMirror.keyName(e), up;
|
||||
var keyName = CodeMirror.keyName(e), up, offset;
|
||||
if (keyName == 'Esc' || keyName == 'Ctrl-C' || keyName == 'Ctrl-[' ||
|
||||
(keyName == 'Backspace' && input == '')) {
|
||||
vimGlobalState.exCommandHistoryController.pushInput(input);
|
||||
|
@ -1355,9 +1359,12 @@
|
|||
cm.focus();
|
||||
}
|
||||
if (keyName == 'Up' || keyName == 'Down') {
|
||||
CodeMirror.e_stop(e);
|
||||
up = keyName == 'Up' ? true : false;
|
||||
offset = e.target ? e.target.selectionEnd : 0;
|
||||
input = vimGlobalState.exCommandHistoryController.nextMatch(input, up) || '';
|
||||
close(input);
|
||||
if (offset && e.target) e.target.selectionEnd = e.target.selectionStart = Math.min(offset, e.target.value.length);
|
||||
} else if (keyName == 'Ctrl-U') {
|
||||
// Ctrl-U clears input.
|
||||
CodeMirror.e_stop(e);
|
||||
|
@ -1620,9 +1627,8 @@
|
|||
return findNext(cm, prev/** prev */, query, motionArgs.repeat);
|
||||
},
|
||||
goToMark: function(cm, _head, motionArgs, vim) {
|
||||
var mark = vim.marks[motionArgs.selectedCharacter];
|
||||
if (mark) {
|
||||
var pos = mark.find();
|
||||
var pos = getMarkPos(cm, vim, motionArgs.selectedCharacter);
|
||||
if (pos) {
|
||||
return motionArgs.linewise ? { line: pos.line, ch: findFirstNonWhiteSpaceCharacter(cm.getLine(pos.line)) } : pos;
|
||||
}
|
||||
return null;
|
||||
|
@ -3966,6 +3972,17 @@
|
|||
return {top: from.line, bottom: to.line};
|
||||
}
|
||||
|
||||
function getMarkPos(cm, vim, markName) {
|
||||
if (markName == '\'') {
|
||||
var history = cm.doc.history.done;
|
||||
var event = history[history.length - 2];
|
||||
return event && event.ranges && event.ranges[0].head;
|
||||
}
|
||||
|
||||
var mark = vim.marks[markName];
|
||||
return mark && mark.find();
|
||||
}
|
||||
|
||||
var ExCommandDispatcher = function() {
|
||||
this.buildCommandMap_();
|
||||
};
|
||||
|
@ -4074,11 +4091,10 @@
|
|||
case '$':
|
||||
return cm.lastLine();
|
||||
case '\'':
|
||||
var mark = cm.state.vim.marks[inputStream.next()];
|
||||
if (mark && mark.find()) {
|
||||
return mark.find().line;
|
||||
}
|
||||
throw new Error('Mark not set');
|
||||
var markName = inputStream.next();
|
||||
var markPos = getMarkPos(cm, cm.state.vim, markName);
|
||||
if (!markPos) throw new Error('Mark not set');
|
||||
return markPos.line;
|
||||
default:
|
||||
inputStream.backUp(1);
|
||||
return undefined;
|
||||
|
@ -4147,8 +4163,8 @@
|
|||
var mapping = {
|
||||
keys: lhs,
|
||||
type: 'keyToEx',
|
||||
exArgs: { input: rhs.substring(1) },
|
||||
user: true};
|
||||
exArgs: { input: rhs.substring(1) }
|
||||
};
|
||||
if (ctx) { mapping.context = ctx; }
|
||||
defaultKeymap.unshift(mapping);
|
||||
} else {
|
||||
|
@ -4156,8 +4172,7 @@
|
|||
var mapping = {
|
||||
keys: lhs,
|
||||
type: 'keyToKey',
|
||||
toKeys: rhs,
|
||||
user: true
|
||||
toKeys: rhs
|
||||
};
|
||||
if (ctx) { mapping.context = ctx; }
|
||||
defaultKeymap.unshift(mapping);
|
||||
|
@ -4178,8 +4193,7 @@
|
|||
var keys = lhs;
|
||||
for (var i = 0; i < defaultKeymap.length; i++) {
|
||||
if (keys == defaultKeymap[i].keys
|
||||
&& defaultKeymap[i].context === ctx
|
||||
&& defaultKeymap[i].user) {
|
||||
&& defaultKeymap[i].context === ctx) {
|
||||
defaultKeymap.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
|
@ -4310,25 +4324,27 @@
|
|||
showConfirm(cm, regInfo);
|
||||
},
|
||||
sort: function(cm, params) {
|
||||
var reverse, ignoreCase, unique, number;
|
||||
var reverse, ignoreCase, unique, number, pattern;
|
||||
function parseArgs() {
|
||||
if (params.argString) {
|
||||
var args = new CodeMirror.StringStream(params.argString);
|
||||
if (args.eat('!')) { reverse = true; }
|
||||
if (args.eol()) { return; }
|
||||
if (!args.eatSpace()) { return 'Invalid arguments'; }
|
||||
var opts = args.match(/[a-z]+/);
|
||||
if (opts) {
|
||||
opts = opts[0];
|
||||
ignoreCase = opts.indexOf('i') != -1;
|
||||
unique = opts.indexOf('u') != -1;
|
||||
var decimal = opts.indexOf('d') != -1 && 1;
|
||||
var hex = opts.indexOf('x') != -1 && 1;
|
||||
var octal = opts.indexOf('o') != -1 && 1;
|
||||
var opts = args.match(/([dinuox]+)?\s*(\/.+\/)?\s*/);
|
||||
if (!opts && !args.eol()) { return 'Invalid arguments'; }
|
||||
if (opts[1]) {
|
||||
ignoreCase = opts[1].indexOf('i') != -1;
|
||||
unique = opts[1].indexOf('u') != -1;
|
||||
var decimal = opts[1].indexOf('d') != -1 || opts[1].indexOf('n') != -1 && 1;
|
||||
var hex = opts[1].indexOf('x') != -1 && 1;
|
||||
var octal = opts[1].indexOf('o') != -1 && 1;
|
||||
if (decimal + hex + octal > 1) { return 'Invalid arguments'; }
|
||||
number = decimal && 'decimal' || hex && 'hex' || octal && 'octal';
|
||||
}
|
||||
if (args.match(/\/.*\//)) { return 'patterns not supported'; }
|
||||
if (opts[2]) {
|
||||
pattern = new RegExp(opts[2].substr(1, opts[2].length - 2), ignoreCase ? 'i' : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
var err = parseArgs();
|
||||
|
@ -4342,14 +4358,18 @@
|
|||
var curStart = Pos(lineStart, 0);
|
||||
var curEnd = Pos(lineEnd, lineLength(cm, lineEnd));
|
||||
var text = cm.getRange(curStart, curEnd).split('\n');
|
||||
var numberRegex = (number == 'decimal') ? /(-?)([\d]+)/ :
|
||||
var numberRegex = pattern ? pattern :
|
||||
(number == 'decimal') ? /(-?)([\d]+)/ :
|
||||
(number == 'hex') ? /(-?)(?:0x)?([0-9a-f]+)/i :
|
||||
(number == 'octal') ? /([0-7]+)/ : null;
|
||||
var radix = (number == 'decimal') ? 10 : (number == 'hex') ? 16 : (number == 'octal') ? 8 : null;
|
||||
var numPart = [], textPart = [];
|
||||
if (number) {
|
||||
if (number || pattern) {
|
||||
for (var i = 0; i < text.length; i++) {
|
||||
if (numberRegex.exec(text[i])) {
|
||||
var matchPart = pattern ? text[i].match(pattern) : null;
|
||||
if (matchPart && matchPart[0] != '') {
|
||||
numPart.push(matchPart);
|
||||
} else if (!pattern && numberRegex.exec(text[i])) {
|
||||
numPart.push(text[i]);
|
||||
} else {
|
||||
textPart.push(text[i]);
|
||||
|
@ -4368,8 +4388,17 @@
|
|||
bnum = parseInt((bnum[1] + bnum[2]).toLowerCase(), radix);
|
||||
return anum - bnum;
|
||||
}
|
||||
numPart.sort(compareFn);
|
||||
textPart.sort(compareFn);
|
||||
function comparePatternFn(a, b) {
|
||||
if (reverse) { var tmp; tmp = a; a = b; b = tmp; }
|
||||
if (ignoreCase) { a[0] = a[0].toLowerCase(); b[0] = b[0].toLowerCase(); }
|
||||
return (a[0] < b[0]) ? -1 : 1;
|
||||
}
|
||||
numPart.sort(pattern ? comparePatternFn : compareFn);
|
||||
if (pattern) {
|
||||
for (var i = 0; i < numPart.length; i++) {
|
||||
numPart[i] = numPart[i].input;
|
||||
}
|
||||
} else if (!number) { textPart.sort(compareFn); }
|
||||
text = (!reverse) ? textPart.concat(numPart) : numPart.concat(textPart);
|
||||
if (unique) { // Remove duplicate lines
|
||||
var textOld = text;
|
||||
|
|
9
codemirror/lib/codemirror.css
vendored
|
@ -223,11 +223,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
|||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
|
@ -272,6 +269,8 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
|
|||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-rtl pre { direction: rtl; }
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
|
8
codemirror/mode/css/css.js
vendored
|
@ -28,6 +28,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
colorKeywords = parserConfig.colorKeywords || {},
|
||||
valueKeywords = parserConfig.valueKeywords || {},
|
||||
allowNested = parserConfig.allowNested,
|
||||
lineComment = parserConfig.lineComment,
|
||||
supportsAtComponent = parserConfig.supportsAtComponent === true;
|
||||
|
||||
var type, override;
|
||||
|
@ -253,6 +254,8 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
};
|
||||
|
||||
states.pseudo = function(type, stream, state) {
|
||||
if (type == "meta") return "pseudo";
|
||||
|
||||
if (type == "word") {
|
||||
override = "variable-3";
|
||||
return state.context.type;
|
||||
|
@ -407,6 +410,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
electricChars: "}",
|
||||
blockCommentStart: "/*",
|
||||
blockCommentEnd: "*/",
|
||||
lineComment: lineComment,
|
||||
fold: "brace"
|
||||
};
|
||||
});
|
||||
|
@ -663,7 +667,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
"small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali",
|
||||
"source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square",
|
||||
"square-button", "start", "static", "status-bar", "stretch", "stroke", "sub",
|
||||
"subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table",
|
||||
"subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "system-ui", "table",
|
||||
"table-caption", "table-cell", "table-column", "table-column-group",
|
||||
"table-footer-group", "table-header-group", "table-row", "table-row-group",
|
||||
"tamil",
|
||||
|
@ -730,6 +734,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
valueKeywords: valueKeywords,
|
||||
fontProperties: fontProperties,
|
||||
allowNested: true,
|
||||
lineComment: "//",
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (stream.eat("/")) {
|
||||
|
@ -772,6 +777,7 @@ CodeMirror.defineMode("css", function(config, parserConfig) {
|
|||
valueKeywords: valueKeywords,
|
||||
fontProperties: fontProperties,
|
||||
allowNested: true,
|
||||
lineComment: "//",
|
||||
tokenHooks: {
|
||||
"/": function(stream, state) {
|
||||
if (stream.eat("/")) {
|
||||
|
|
2
codemirror/mode/css/index.html
vendored
|
@ -64,7 +64,7 @@ code {
|
|||
</textarea></form>
|
||||
<script>
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
|
||||
extraKeys: {"Ctrl-Space": "autocomplete"},
|
||||
extraKeys: {"Ctrl-Space": "autocomplete"}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
12
csslint/WARNING.txt
Normal file
|
@ -0,0 +1,12 @@
|
|||
1. Until https://github.com/CSSLint/parser-lib/issues/229 is fixed, manually replace:
|
||||
|
||||
while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) {
|
||||
|
||||
in "_function: function()" with
|
||||
|
||||
while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) {
|
||||
|
||||
2. Apply our hacks unless supported natively:
|
||||
|
||||
* Support :any(), :-webkit-any(), :-moz-any()
|
||||
* Support @supports inside @-moz-document
|
|
@ -2781,7 +2781,7 @@ Parser.prototype = function() {
|
|||
|
||||
//functionText += this._term();
|
||||
lt = tokenStream.peek();
|
||||
while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) {
|
||||
while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN && lt !== Tokens.EOF) {
|
||||
tokenStream.get();
|
||||
functionText += tokenStream.token().value;
|
||||
lt = tokenStream.peek();
|
||||
|
|
110
dom.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
'use strict';
|
||||
|
||||
if (!navigator.userAgent.includes('Windows')) {
|
||||
document.documentElement.classList.add('non-windows');
|
||||
}
|
||||
|
||||
// polyfill for old browsers to enable [...results] and for-of
|
||||
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
|
||||
if (!type.prototype[Symbol.iterator]) {
|
||||
type.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState != 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function scrollElementIntoView(element) {
|
||||
// align to the top/bottom of the visible area if wasn't visible
|
||||
const bounds = element.getBoundingClientRect();
|
||||
if (bounds.top < 0 || bounds.top > innerHeight - bounds.height) {
|
||||
element.scrollIntoView(bounds.top < 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function animateElement(element, {className, remove = false}) {
|
||||
return new Promise(resolve => {
|
||||
element.addEventListener('animationend', function _() {
|
||||
element.removeEventListener('animationend', _);
|
||||
element.classList.remove(className);
|
||||
// TODO: investigate why animation restarts if the elements is removed in .then()
|
||||
if (remove) {
|
||||
element.remove();
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
element.classList.add(className);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function enforceInputRange(element) {
|
||||
const min = Number(element.min);
|
||||
const max = Number(element.max);
|
||||
const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true}));
|
||||
const onChange = ({type}) => {
|
||||
if (type == 'input' && element.checkValidity()) {
|
||||
doNotify();
|
||||
} else if (type == 'change' && !element.checkValidity()) {
|
||||
element.value = Math.max(min, Math.min(max, Number(element.value)));
|
||||
doNotify();
|
||||
}
|
||||
};
|
||||
element.addEventListener('change', onChange);
|
||||
element.addEventListener('input', onChange);
|
||||
}
|
||||
|
||||
|
||||
function $(selector, base = document) {
|
||||
// we have ids with . like #manage.onlyEnabled which looks like #id.class
|
||||
// so since getElementById is superfast we'll try it anyway
|
||||
const byId = selector.startsWith('#') && document.getElementById(selector.slice(1));
|
||||
return byId || base.querySelector(selector);
|
||||
}
|
||||
|
||||
|
||||
function $$(selector, base = document) {
|
||||
return [...base.querySelectorAll(selector)];
|
||||
}
|
||||
|
||||
|
||||
function $element(opt) {
|
||||
// tag: string, default 'div', may include namespace like 'ns#tag'
|
||||
// appendChild: element or an array of elements
|
||||
// dataset: object
|
||||
// any DOM property: assigned as is
|
||||
const [ns, tag] = opt.tag && opt.tag.includes('#')
|
||||
? opt.tag.split('#')
|
||||
: [null, opt.tag];
|
||||
const element = ns
|
||||
? document.createElementNS(ns == 'SVG' || ns == 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
|
||||
: document.createElement(tag || 'div');
|
||||
(opt.appendChild instanceof Array ? opt.appendChild : [opt.appendChild])
|
||||
.forEach(child => child && element.appendChild(child));
|
||||
delete opt.appendChild;
|
||||
delete opt.tag;
|
||||
if (opt.dataset) {
|
||||
Object.assign(element.dataset, opt.dataset);
|
||||
delete opt.dataset;
|
||||
}
|
||||
if (ns) {
|
||||
for (const attr in opt) {
|
||||
element.setAttributeNS(null, attr, opt[attr]);
|
||||
}
|
||||
} else {
|
||||
Object.assign(element, opt);
|
||||
}
|
||||
return element;
|
||||
}
|
182
edit.html
|
@ -1,6 +1,14 @@
|
|||
<html>
|
||||
<html id="stylus">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="edit.js"></script>
|
||||
|
||||
<script src="codemirror/lib/codemirror.js"></script>
|
||||
<link rel="stylesheet" href="codemirror/lib/codemirror.css">
|
||||
<script src="codemirror/mode/css/css.js"></script>
|
||||
|
@ -9,6 +17,7 @@
|
|||
<link rel="stylesheet" href="codemirror/addon/search/matchesonscrollbar.css">
|
||||
<script src="codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
<script src="codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="codemirror-overwrites/addon/search/match-highlighter.js"></script>
|
||||
<script src="codemirror/addon/dialog/dialog.js"></script>
|
||||
<script src="codemirror/addon/search/searchcursor.js"></script>
|
||||
<script src="codemirror/addon/search/search.js"></script>
|
||||
|
@ -40,27 +49,29 @@
|
|||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 9pt arial,sans-serif;
|
||||
font: 12px arial,sans-serif;
|
||||
}
|
||||
/************ header ************/
|
||||
#header {
|
||||
height: calc(100vh - 30px);
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
width: 15rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 0.95rem;
|
||||
padding: 15px;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 3rem -1.2rem black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
#sections {
|
||||
padding-left: 18rem;
|
||||
padding-left: 280px;
|
||||
}
|
||||
#sections h2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
margin-left: 1.7rem;
|
||||
}
|
||||
.aligned {
|
||||
display: table-row;
|
||||
|
@ -90,23 +101,39 @@
|
|||
#url:not([href^="http"]) {
|
||||
display: none;
|
||||
}
|
||||
#save-button {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dirty #save-button {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
.svg-icon {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: fill .5s;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:not(.applies-to-help):not(.dismiss) {
|
||||
.svg-icon:not(.dismiss) {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
h2 .svg-icon, label .svg-icon {
|
||||
margin-top: -2px;
|
||||
}
|
||||
.svg-icon.info:hover {
|
||||
fill: #000000;
|
||||
.svg-icon.info {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
.svg-icon:hover,
|
||||
.svg-icon.info {
|
||||
fill: #666;
|
||||
}
|
||||
.svg-icon,
|
||||
.svg-icon.info:hover {
|
||||
fill: #000;
|
||||
}
|
||||
a:hover .svg-icon.installed, .svg-icon.dismiss:hover {
|
||||
fill: hsl(0, 0%, 40%);
|
||||
}
|
||||
#enabled {
|
||||
margin-left: 0;
|
||||
vertical-align: middle;
|
||||
|
@ -183,6 +210,15 @@
|
|||
.CodeMirror-search-hint {
|
||||
color: #888;
|
||||
}
|
||||
body[data-highlight-selection-matches="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-highlight-selection-matches="token"] .CodeMirror-selection-highlight-scrollbar {
|
||||
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
body[data-highlight-selection-matches="selection"] .cm-matchhighlight-approved .cm-matchhighlight,
|
||||
body[data-highlight-selection-matches="selection"] .CodeMirror-selection-highlight-scrollbar {
|
||||
background-color: rgba(1, 151, 193, 0.1);
|
||||
}
|
||||
@-webkit-keyframes highlight {
|
||||
from {
|
||||
background-color: #ff9;
|
||||
|
@ -191,6 +227,18 @@
|
|||
background-color: none;
|
||||
}
|
||||
}
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadein-match-highlighter {
|
||||
from { background-color: transparent; }
|
||||
to { background-color: rgba(1, 151, 193, 0.1); }
|
||||
}
|
||||
.resize-grip {
|
||||
position: absolute;
|
||||
display: block;
|
||||
|
@ -245,6 +293,62 @@
|
|||
.applies-to img {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.test-regexp {
|
||||
display: none;
|
||||
}
|
||||
.has-regexp .test-regexp {
|
||||
display: inline-block;
|
||||
}
|
||||
.regexp-report summary, .regexp-report div {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.regexp-report mark {
|
||||
background-color: rgba(255, 255, 0, .5);
|
||||
}
|
||||
.regexp-report details {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.regexp-report details:not(:last-child) {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.regexp-report summary {
|
||||
font-weight: bold;
|
||||
margin-left: -1rem;
|
||||
margin-bottom: .5rem;
|
||||
outline: none;
|
||||
cursor: default;
|
||||
}
|
||||
.regexp-report details[data-type="full"] {
|
||||
color: darkgreen;
|
||||
}
|
||||
.regexp-report details[data-type="partial"] {
|
||||
color: darkgray;
|
||||
}
|
||||
.regexp-report details[data-type="invalid"] {
|
||||
color: maroon;
|
||||
}
|
||||
.regexp-report details details {
|
||||
margin-left: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.regexp-report .svg-icon {
|
||||
position: absolute;
|
||||
margin-top: -1px;
|
||||
}
|
||||
.regexp-report details div:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
.regexp-report details div img {
|
||||
width: 16px;
|
||||
max-height: 16px;
|
||||
position: absolute;
|
||||
margin-left: -20px;
|
||||
margin-top: -1px;
|
||||
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
/************ help popup ************/
|
||||
#help-popup {
|
||||
top: 3rem;
|
||||
|
@ -269,16 +373,16 @@
|
|||
font-weight: bold;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
margin: -0.5rem -0.5rem 0.5rem;
|
||||
padding: 0.5rem;
|
||||
padding: .5rem 32px .5rem .5rem;
|
||||
}
|
||||
#help-popup .contents {
|
||||
max-height: calc(100vh - 8rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
#help-popup .close-icon {
|
||||
#help-popup .dismiss {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
top: .5em;
|
||||
}
|
||||
|
||||
.keymap-list {
|
||||
|
@ -514,13 +618,14 @@
|
|||
<br>
|
||||
<div class="applies-to">
|
||||
<label i18n-text="appliesLabel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="applies-to-help svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg>
|
||||
<svg class="svg-icon info applies-to-help"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</label>
|
||||
<ul class="applies-to-list"></ul>
|
||||
</div>
|
||||
<button class="remove-section" i18n-text="sectionRemove"></button>
|
||||
<button class="add-section" i18n-text="sectionAdd"></button>
|
||||
<button class="beautify-section" i18n-text="styleBeautify"></button>
|
||||
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
|
||||
</div>
|
||||
</template>
|
||||
<template data-id="find">
|
||||
|
@ -552,20 +657,18 @@
|
|||
<template data-id="jumpToLine">
|
||||
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
|
||||
</template>
|
||||
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="edit.js"></script>
|
||||
<template data-id="regexpTestPartial">
|
||||
<a target="_blank" href="https://github.com/stylish-userstyles/stylish/wiki/Applying-styles-to-specific-sites#advanced-matching-with-regular-expressions"><svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></a>
|
||||
</template>
|
||||
</head>
|
||||
|
||||
<body id="stylus-edit">
|
||||
<div id="header">
|
||||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" i18n-placeholder="styleMissingName">
|
||||
<a id="url" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon installed" fill="#000000" height="16" width="16" viewBox="0 0 8 8"><path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path></svg></a>
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
<input type="checkbox" id="enabled" class="style-contributor">
|
||||
|
@ -579,7 +682,7 @@
|
|||
<a href="manage.html"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
|
||||
</div>
|
||||
<div>
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="to-mozilla-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2>
|
||||
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading"><svg id="to-mozilla-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
<button id="from-mozilla" i18n-text="importLabel"></button>
|
||||
<button id="to-mozilla" i18n-text="exportLabel"></button>
|
||||
</div>
|
||||
|
@ -605,21 +708,42 @@
|
|||
<div class="option aligned">
|
||||
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
|
||||
<select data-option="keyMap" id="editor.keyMap"></select>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="keyMap-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg>
|
||||
<svg id="keyMap-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
|
||||
<select data-option="theme" id="editor.theme"></select>
|
||||
</div>
|
||||
<div class="option aligned">
|
||||
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
|
||||
<select data-option="highlightSelectionMatches" id="editor.matchHighlight">
|
||||
<option i18n-text="cm_matchHighlightToken" value="token">
|
||||
<option i18n-text="cm_matchHighlightSelection" value="selection">
|
||||
<option i18n-text="genericDisabledLabel" value="">
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="lint-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2><div></div></section>
|
||||
<section id="lint"><h2 i18n-text="issues">: <span id="issue-count"></span><svg id="lint-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2><div></div></section>
|
||||
</div>
|
||||
<section id="sections">
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="sections-help" class="svg-icon info" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></h2>
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span><svg id="sections-help" class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg></h2>
|
||||
</section>
|
||||
<div id="help-popup">
|
||||
<div class="title"></div><svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="close-icon svg-icon dismiss" fill="#000000" height="16" width="12" viewBox="0 0 12 16"><path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path></svg>
|
||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg></svg>
|
||||
<div class="contents"></div>
|
||||
</div>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none">
|
||||
<symbol id="svg-icon-external-link" height="16" width="16" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-help" height="16" width="14" viewBox="0 0 14 16" i18n-alt="helpAlt">
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</symbol>
|
||||
<symbol id="svg-icon-close" height="16" width="12" viewBox="0 0 12 16">
|
||||
<path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48z"></path>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
472
edit.js
|
@ -1,4 +1,5 @@
|
|||
/* globals stringAsRegExp */
|
||||
/* eslint no-tabs: 0, no-var: 0, indent: [2, tab, {VariableDeclarator: 0, SwitchCase: 1}], quotes: 0 */
|
||||
/* global CodeMirror */
|
||||
"use strict";
|
||||
|
||||
var styleId = null;
|
||||
|
@ -11,6 +12,9 @@ var useHistoryBack; // use browser history back when "back to manage" is click
|
|||
var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", regexps: "regexp"};
|
||||
var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"};
|
||||
|
||||
// if background page hasn't been loaded yet, increase the chances it has before DOMContentLoaded
|
||||
onBackgroundReady();
|
||||
|
||||
// make querySelectorAll enumeration code readable
|
||||
["forEach", "some", "indexOf", "map"].forEach(function(method) {
|
||||
NodeList.prototype[method]= Array.prototype[method];
|
||||
|
@ -29,10 +33,22 @@ Array.prototype.rotate = function(amount) { // negative amount == rotate left
|
|||
var r = this.slice(-amount, this.length);
|
||||
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(Array.prototype, "last", {get: function() { return this[this.length - 1]; }});
|
||||
|
||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||
new MutationObserver((mutations, observer) => {
|
||||
const themeElement = document.getElementById("cm-theme");
|
||||
if (themeElement) {
|
||||
themeElement.href = prefs.get("editor.theme") == "default" ? ""
|
||||
: "codemirror/theme/" + prefs.get("editor.theme") + ".css";
|
||||
observer.disconnect();
|
||||
}
|
||||
}).observe(document, {subtree: true, childList: true});
|
||||
|
||||
getCodeMirrorThemes();
|
||||
|
||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||
var hotkeyRerouter = {
|
||||
commands: {
|
||||
|
@ -130,6 +146,11 @@ function initCodeMirror() {
|
|||
var CM = CodeMirror;
|
||||
var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0;
|
||||
|
||||
// CodeMirror miserably fails on keyMap="" so let's ensure it's not
|
||||
if (!prefs.get('editor.keyMap')) {
|
||||
prefs.reset('editor.keyMap');
|
||||
}
|
||||
|
||||
// default option values
|
||||
Object.assign(CM.defaults, {
|
||||
mode: 'css',
|
||||
|
@ -138,6 +159,7 @@ function initCodeMirror() {
|
|||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"],
|
||||
matchBrackets: true,
|
||||
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
||||
lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")},
|
||||
lintReportDelay: prefs.get("editor.lintReportDelay"),
|
||||
styleActiveLine: true,
|
||||
|
@ -229,38 +251,30 @@ function initCodeMirror() {
|
|||
return this.display.wrapper.parentNode;
|
||||
};
|
||||
|
||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||
var theme = prefs.get("editor.theme");
|
||||
document.getElementById("cm-theme").href = theme == "default" ? "" : "codemirror/theme/" + theme + ".css";
|
||||
|
||||
// initialize global editor controls
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
function optionsHtmlFromArray(options) {
|
||||
return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
|
||||
}
|
||||
var themeControl = document.getElementById("editor.theme");
|
||||
var bg = chrome.extension.getBackgroundPage();
|
||||
if (bg && bg.codeMirrorThemes) {
|
||||
themeControl.innerHTML = optionsHtmlFromArray(bg.codeMirrorThemes);
|
||||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
|
||||
getCodeMirrorThemes(function(themes) {
|
||||
themeControl.innerHTML = optionsHtmlFromArray(themes);
|
||||
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
|
||||
document.getElementById("options").addEventListener("change", acmeEventListener, false);
|
||||
setupLivePrefs(
|
||||
document.querySelectorAll("#options *[data-option][id^='editor.']")
|
||||
.map(function(option) { return option.id })
|
||||
);
|
||||
});
|
||||
function optionsHtmlFromArray(options) {
|
||||
return options.map(function(opt) { return "<option>" + opt + "</option>"; }).join("");
|
||||
}
|
||||
var themeControl = document.getElementById("editor.theme");
|
||||
const themeList = localStorage.codeMirrorThemes;
|
||||
if (themeList) {
|
||||
themeControl.innerHTML = optionsHtmlFromArray(themeList.split(/\s+/));
|
||||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
const theme = prefs.get("editor.theme");
|
||||
themeControl.innerHTML = optionsHtmlFromArray([theme == "default" ? t("defaultTheme") : theme]);
|
||||
getCodeMirrorThemes().then(() => {
|
||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||
themeControl.innerHTML = optionsHtmlFromArray(themes);
|
||||
themeControl.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort());
|
||||
document.getElementById("options").addEventListener("change", acmeEventListener, false);
|
||||
setupLivePrefs();
|
||||
|
||||
hotkeyRerouter.setState(true);
|
||||
}
|
||||
initCodeMirror();
|
||||
|
||||
function acmeEventListener(event) {
|
||||
var el = event.target;
|
||||
|
@ -287,7 +301,7 @@ function acmeEventListener(event) {
|
|||
el.selectedIndex = 0;
|
||||
break;
|
||||
}
|
||||
var url = chrome.extension.getURL("codemirror/theme/" + value + ".css");
|
||||
var url = chrome.runtime.getURL("codemirror/theme/" + value + ".css");
|
||||
if (themeLink.href == url) { // preloaded in initCodeMirror()
|
||||
break;
|
||||
}
|
||||
|
@ -302,13 +316,23 @@ function acmeEventListener(event) {
|
|||
}, 100);
|
||||
})();
|
||||
return;
|
||||
case "highlightSelectionMatches":
|
||||
switch (value) {
|
||||
case 'token':
|
||||
case 'selection':
|
||||
document.body.dataset[option] = value;
|
||||
value = {showToken: value == 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
}
|
||||
}
|
||||
CodeMirror.setOption(option, value);
|
||||
}
|
||||
|
||||
// replace given textarea with the CodeMirror editor
|
||||
function setupCodeMirror(textarea, index) {
|
||||
var cm = CodeMirror.fromTextArea(textarea);
|
||||
var cm = CodeMirror.fromTextArea(textarea, {lint: null});
|
||||
|
||||
cm.on("change", indicateCodeChange);
|
||||
cm.on("blur", function(cm) {
|
||||
|
@ -324,6 +348,7 @@ function setupCodeMirror(textarea, index) {
|
|||
hotkeyRerouter.setState(false);
|
||||
cm.display.wrapper.classList.add("CodeMirror-active");
|
||||
});
|
||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||
|
||||
var resizeGrip = cm.display.wrapper.appendChild(document.createElement("div"));
|
||||
resizeGrip.className = "resize-grip";
|
||||
|
@ -395,6 +420,10 @@ document.addEventListener("wheel", function(event) {
|
|||
chrome.tabs.query({currentWindow: true}, function(tabs) {
|
||||
var windowId = tabs[0].windowId;
|
||||
if (prefs.get("openEditInWindow")) {
|
||||
if (sessionStorage.saveSizeOnClose && 'left' in prefs.get('windowPosition', {})) {
|
||||
// window was reopened via Ctrl-Shift-T etc.
|
||||
chrome.windows.update(windowId, prefs.get('windowPosition'));
|
||||
}
|
||||
if (tabs.length == 1 && window.history.length == 1) {
|
||||
chrome.windows.getAll(function(windows) {
|
||||
if (windows.length > 1) {
|
||||
|
@ -414,7 +443,7 @@ chrome.tabs.query({currentWindow: true}, function(tabs) {
|
|||
});
|
||||
});
|
||||
|
||||
getActiveTab(function(tab) {
|
||||
getActiveTab().then(tab => {
|
||||
useHistoryBack = sessionStorageHash("manageStylesHistory").value[tab.id] == location.href;
|
||||
});
|
||||
|
||||
|
@ -497,6 +526,23 @@ function addSection(event, section) {
|
|||
appliesTo.addEventListener("change", onChange);
|
||||
appliesTo.addEventListener("input", onChange);
|
||||
|
||||
toggleTestRegExpVisibility();
|
||||
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
||||
div.querySelector('.test-regexp').onclick = showRegExpTester;
|
||||
function toggleTestRegExpVisibility() {
|
||||
const show = [...appliesTo.children].some(item =>
|
||||
!item.matches('.applies-to-everything') &&
|
||||
item.querySelector('.applies-type').value == 'regexp' &&
|
||||
item.querySelector('.applies-value').value.trim());
|
||||
div.classList.toggle('has-regexp', show);
|
||||
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
||||
if (event.target.matches('.applies-value')
|
||||
&& event.target.parentElement.querySelector('.applies-type').value == 'regexp') {
|
||||
showRegExpTester(null, div);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var sections = document.getElementById("sections");
|
||||
if (event) {
|
||||
var clickedSection = getSectionForChild(event.target);
|
||||
|
@ -857,36 +903,32 @@ function getEditorInSight(nearbyElement) {
|
|||
function updateLintReport(cm, delay) {
|
||||
if (delay == 0) {
|
||||
// immediately show pending csslint messages in onbeforeunload and save
|
||||
update.call(cm);
|
||||
update(cm);
|
||||
return;
|
||||
}
|
||||
if (delay > 0) {
|
||||
// give csslint some time to find the issues, e.g. 500 (1/10 of our default 5s)
|
||||
// by settings its internal delay to 1ms and restoring it back later
|
||||
var lintOpt = editors[0].state.lint.options;
|
||||
setTimeout((function(opt, delay) {
|
||||
opt.delay = delay == 1 ? opt.delay : delay; // options object is shared between editors
|
||||
update(this);
|
||||
}).bind(cm, lintOpt, lintOpt.delay), delay);
|
||||
lintOpt.delay = 1;
|
||||
setTimeout(cm => { cm.performLint(); update(cm) }, delay, cm);
|
||||
return;
|
||||
}
|
||||
var state = cm.state.lint;
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
// user is editing right now: postpone updating the report for the new issues (default: 500ms lint + 4500ms)
|
||||
// or update it as soon as possible (default: 500ms lint + 100ms) in case an existing issue was just fixed
|
||||
var state = cm.state.lint;
|
||||
clearTimeout(state.reportTimeout);
|
||||
state.reportTimeout = setTimeout(update.bind(cm), state.options.delay + 100);
|
||||
state.reportTimeout = setTimeout(update, state.options.delay + 100, cm);
|
||||
state.postponeNewIssues = delay == undefined || delay == null;
|
||||
|
||||
function update() { // this == cm
|
||||
var scope = this ? [this] : editors;
|
||||
function update(cm) {
|
||||
var scope = cm ? [cm] : editors;
|
||||
var changed = false;
|
||||
var fixedOldIssues = false;
|
||||
scope.forEach(function(cm) {
|
||||
var state = cm.state.lint;
|
||||
var state = cm.state.lint || {};
|
||||
var oldMarkers = state.markedLast || {};
|
||||
var newMarkers = {};
|
||||
var html = state.marked.length == 0 ? "" : "<tbody>" +
|
||||
var html = !state.marked || state.marked.length == 0 ? "" : "<tbody>" +
|
||||
state.marked.map(function(mark) {
|
||||
var info = mark.__annotation;
|
||||
var isActiveLine = info.from.line == cm.getCursor().line;
|
||||
|
@ -938,7 +980,7 @@ function renderLintReport(someBlockChanged) {
|
|||
var newContent = content.cloneNode(false);
|
||||
var issueCount = 0;
|
||||
editors.forEach(function(cm, index) {
|
||||
if (cm.state.lint.html) {
|
||||
if (cm.state.lint && cm.state.lint.html) {
|
||||
var newBlock = newContent.appendChild(document.createElement("table"));
|
||||
var html = "<caption>" + label + " " + (index+1) + "</caption>" + cm.state.lint.html;
|
||||
newBlock.innerHTML = html;
|
||||
|
@ -993,7 +1035,7 @@ function beautify(event) {
|
|||
doBeautify();
|
||||
} else {
|
||||
var script = document.head.appendChild(document.createElement("script"));
|
||||
script.src = "beautify/beautify-css.js";
|
||||
script.src = "beautify/beautify-css-mod.js";
|
||||
script.onload = doBeautify;
|
||||
}
|
||||
function doBeautify() {
|
||||
|
@ -1023,6 +1065,7 @@ function beautify(event) {
|
|||
if (cm.beautifyChange && cm.beautifyChange[cm.changeGeneration()]) {
|
||||
delete cm.beautifyChange[cm.changeGeneration()];
|
||||
cm.undo();
|
||||
cm.scrollIntoView(cm.getCursor());
|
||||
undoable |= cm.beautifyChange[cm.changeGeneration()];
|
||||
}
|
||||
});
|
||||
|
@ -1031,6 +1074,9 @@ function beautify(event) {
|
|||
|
||||
scope.forEach(function(cm) {
|
||||
setTimeout(function() {
|
||||
const pos = options.translate_positions =
|
||||
[].concat.apply([], cm.doc.sel.ranges.map(r =>
|
||||
[Object.assign({}, r.anchor), Object.assign({}, r.head)]));
|
||||
var text = cm.getValue();
|
||||
var newText = exports.css_beautify(text, options);
|
||||
if (newText != text) {
|
||||
|
@ -1039,6 +1085,11 @@ function beautify(event) {
|
|||
cm.beautifyChange = {};
|
||||
}
|
||||
cm.setValue(newText);
|
||||
const selections = [];
|
||||
for (let i = 0; i < pos.length; i += 2) {
|
||||
selections.push({anchor: pos[i], head: pos[i + 1]});
|
||||
}
|
||||
cm.setSelections(selections);
|
||||
cm.beautifyChange[cm.changeGeneration()] = true;
|
||||
undoButton.disabled = false;
|
||||
}
|
||||
|
@ -1065,48 +1116,67 @@ function beautify(event) {
|
|||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", init, false);
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
|
||||
function init() {
|
||||
initCodeMirror();
|
||||
var params = getParams();
|
||||
if (!params.id) { // match should be 2 - one for the whole thing, one for the parentheses
|
||||
// This is an add
|
||||
tE("heading", "addStyleTitle");
|
||||
var section = {code: ""}
|
||||
for (var i in CssToProperty) {
|
||||
if (params[i]) {
|
||||
section[CssToProperty[i]] = [params[i]];
|
||||
}
|
||||
}
|
||||
addSection(null, section);
|
||||
// default to enabled
|
||||
document.getElementById("enabled").checked = true
|
||||
tE("heading", "addStyleTitle");
|
||||
initHooks();
|
||||
window.onload = () => {
|
||||
window.onload = null;
|
||||
addSection(null, section);
|
||||
// default to enabled
|
||||
document.getElementById("enabled").checked = true
|
||||
initHooks();
|
||||
};
|
||||
return;
|
||||
}
|
||||
// This is an edit
|
||||
tE("heading", "editStyleHeading", null, false);
|
||||
requestStyle();
|
||||
function requestStyle() {
|
||||
chrome.runtime.sendMessage({method: "getStyles", id: params.id}, function callback(styles) {
|
||||
if (!styles) { // Chrome is starting up and shows edit.html
|
||||
requestStyle();
|
||||
return;
|
||||
}
|
||||
var style = styles[0];
|
||||
styleId = style.id;
|
||||
initWithStyle(style);
|
||||
});
|
||||
}
|
||||
getStylesSafe({id: params.id}).then(styles => {
|
||||
let style = styles[0];
|
||||
if (!style) {
|
||||
style = {id: null, sections: []};
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
styleId = style.id;
|
||||
setStyleMeta(style);
|
||||
window.onload = () => {
|
||||
window.onload = null;
|
||||
initWithStyle({style});
|
||||
};
|
||||
if (document.readyState != 'loading') {
|
||||
window.onload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initWithStyle(style) {
|
||||
function setStyleMeta(style) {
|
||||
document.getElementById("name").value = style.name;
|
||||
document.getElementById("enabled").checked = style.enabled;
|
||||
document.getElementById("url").href = style.url;
|
||||
}
|
||||
|
||||
function initWithStyle({style, codeIsUpdated}) {
|
||||
setStyleMeta(style);
|
||||
|
||||
if (codeIsUpdated === false) {
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
return;
|
||||
}
|
||||
|
||||
// if this was done in response to an update, we need to clear existing sections
|
||||
getSections().forEach(function(div) { div.remove(); });
|
||||
var queue = style.sections.length ? style.sections : [{code: ""}];
|
||||
var queue = style.sections.length ? style.sections.slice() : [{code: ""}];
|
||||
var queueStart = new Date().getTime();
|
||||
// after 100ms the sections will be added asynchronously
|
||||
while (new Date().getTime() - queueStart <= 100 && queue.length) {
|
||||
|
@ -1123,7 +1193,11 @@ function initWithStyle(style) {
|
|||
function add() {
|
||||
var sectionDiv = addSection(null, queue.shift());
|
||||
maximizeCodeHeight(sectionDiv, !queue.length);
|
||||
updateLintReport(sectionDiv.CodeMirror, prefs.get("editor.lintDelay"));
|
||||
const cm = sectionDiv.CodeMirror;
|
||||
setTimeout(() => {
|
||||
cm.setOption('lint', CodeMirror.defaults.lint);
|
||||
updateLintReport(cm, 0);
|
||||
}, prefs.get("editor.lintDelay"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1149,11 +1223,28 @@ function initHooks() {
|
|||
document.querySelector("#lint h2").addEventListener("click", toggleLintReport);
|
||||
}
|
||||
|
||||
document.querySelectorAll(
|
||||
'input:not([type]), input[type="text"], input[type="search"], input[type="number"]')
|
||||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
||||
|
||||
setupGlobalSearch();
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
|
||||
function toggleContextMenuDelete(event) {
|
||||
if (event.button == 2 && prefs.get('editor.contextDelete')) {
|
||||
chrome.contextMenus.update('editor.contextDelete', {
|
||||
enabled: Boolean(
|
||||
this.selectionStart != this.selectionEnd ||
|
||||
this.somethingSelected && this.somethingSelected()
|
||||
),
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function maximizeCodeHeight(sectionDiv, isLast) {
|
||||
var cm = sectionDiv.CodeMirror;
|
||||
var stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
|
||||
|
@ -1254,14 +1345,14 @@ function save() {
|
|||
}
|
||||
var name = document.getElementById("name").value;
|
||||
var enabled = document.getElementById("enabled").checked;
|
||||
var request = {
|
||||
method: "saveStyle",
|
||||
saveStyleSafe({
|
||||
id: styleId,
|
||||
name: name,
|
||||
enabled: enabled,
|
||||
reason: 'editSave',
|
||||
sections: getSectionsHashes()
|
||||
};
|
||||
chrome.runtime.sendMessage(request, saveComplete);
|
||||
})
|
||||
.then(saveComplete);
|
||||
}
|
||||
|
||||
function getSectionsHashes() {
|
||||
|
@ -1348,7 +1439,7 @@ function fromMozillaFormat() {
|
|||
|
||||
function doImport() {
|
||||
var replaceOldStyle = this.name == "import-replace";
|
||||
popup.querySelector(".close-icon").click();
|
||||
popup.querySelector(".dismiss").onclick();
|
||||
var mozStyle = trimNewLines(popup.codebox.getValue());
|
||||
var parser = new parserlib.css.Parser(), lines = mozStyle.split("\n");
|
||||
var sectionStack = [{code: "", start: {line: 1, col: 1}}];
|
||||
|
@ -1423,15 +1514,20 @@ function fromMozillaFormat() {
|
|||
}
|
||||
}
|
||||
function doAddSection(section) {
|
||||
section.code = section.code.trim();
|
||||
// don't add empty sections
|
||||
if (!section.code
|
||||
&& !section.urls
|
||||
&& !section.urlPrefixes
|
||||
&& !section.domains
|
||||
&& !section.regexps) {
|
||||
return;
|
||||
}
|
||||
if (!firstAddedCM) {
|
||||
if (!initFirstSection(section)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// don't add empty sections
|
||||
if (!(section.code || section.urls || section.urlPrefixes || section.domains || section.regexps)) {
|
||||
return;
|
||||
}
|
||||
setCleanItem(addSection(null, section), false);
|
||||
firstAddedCM = firstAddedCM || editors.last;
|
||||
}
|
||||
|
@ -1532,8 +1628,8 @@ function showKeyMapHelp() {
|
|||
cell.innerHTML = cell.textContent;
|
||||
});
|
||||
}
|
||||
function mergeKeyMaps(merged) {
|
||||
[].slice.call(arguments, 1).forEach(function(keyMap) {
|
||||
function mergeKeyMaps(merged, ...more) {
|
||||
more.forEach(keyMap => {
|
||||
if (typeof keyMap == "string") {
|
||||
keyMap = CodeMirror.keyMap[keyMap];
|
||||
}
|
||||
|
@ -1567,6 +1663,111 @@ function showLintHelp() {
|
|||
);
|
||||
}
|
||||
|
||||
function showRegExpTester(event, section = getSectionForChild(this)) {
|
||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||
const cachedRegexps = showRegExpTester.cachedRegexps =
|
||||
showRegExpTester.cachedRegexps || new Map();
|
||||
const regexps = [...section.querySelector('.applies-to-list').children]
|
||||
.map(item =>
|
||||
!item.matches('.applies-to-everything') &&
|
||||
item.querySelector('.applies-type').value == 'regexp' &&
|
||||
item.querySelector('.applies-value').value.trim())
|
||||
.filter(item => item)
|
||||
.map(text => {
|
||||
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
||||
if (!rxData.urls) {
|
||||
cachedRegexps.set(text, Object.assign(rxData, {
|
||||
rx: tryRegExp(text),
|
||||
urls: new Map(),
|
||||
}));
|
||||
}
|
||||
return rxData;
|
||||
});
|
||||
chrome.tabs.onUpdated.addListener(function _(tabId, info) {
|
||||
if (document.querySelector('.regexp-report')) {
|
||||
if (info.url) {
|
||||
showRegExpTester(event, section);
|
||||
}
|
||||
} else {
|
||||
chrome.tabs.onUpdated.removeListener(_);
|
||||
}
|
||||
});
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const supported = tabs.map(tab => tab.url)
|
||||
.filter(url => URLS.supported.test(url));
|
||||
const unique = [...new Set(supported).values()];
|
||||
for (const rxData of regexps) {
|
||||
const {rx, urls} = rxData;
|
||||
if (rx) {
|
||||
const urlsNow = new Map();
|
||||
for (const url of unique) {
|
||||
const match = urls.get(url) || (url.match(rx) || [])[0];
|
||||
if (match) {
|
||||
urlsNow.set(url, match);
|
||||
}
|
||||
}
|
||||
rxData.urls = urlsNow;
|
||||
}
|
||||
}
|
||||
const moreInfoLink = template.regexpTestPartial.outerHTML;
|
||||
const stats = {
|
||||
full: {data: [], label: t('styleRegexpTestFull')},
|
||||
partial: {data: [], label: t('styleRegexpTestPartial') + moreInfoLink},
|
||||
none: {data: [], label: t('styleRegexpTestNone')},
|
||||
invalid: {data: [], label: t('styleRegexpTestInvalid')},
|
||||
};
|
||||
for (const {text, rx, urls} of regexps) {
|
||||
if (!rx) {
|
||||
stats.invalid.data.push({text});
|
||||
continue;
|
||||
}
|
||||
if (!urls.size) {
|
||||
stats.none.data.push({text});
|
||||
continue;
|
||||
}
|
||||
const full = [];
|
||||
const partial = [];
|
||||
for (const [url, match] of urls.entries()) {
|
||||
const faviconUrl = url.startsWith(URLS.ownOrigin)
|
||||
? OWN_ICON
|
||||
: GET_FAVICON_URL + new URL(url).hostname;
|
||||
const icon = `<img src="${faviconUrl}">`;
|
||||
if (match.length == url.length) {
|
||||
full.push(`<div>${icon + url}</div>`);
|
||||
} else {
|
||||
partial.push(`<div>${icon}<mark>${match}</mark>` +
|
||||
url.substr(match.length) + '</div>');
|
||||
}
|
||||
}
|
||||
if (full.length) {
|
||||
stats.full.data.push({text, urls: full});
|
||||
}
|
||||
if (partial.length) {
|
||||
stats.partial.data.push({text, urls: partial});
|
||||
}
|
||||
}
|
||||
showHelp(t('styleRegexpTestTitle'),
|
||||
'<div class="regexp-report">' +
|
||||
Object.keys(stats).map(type => (!stats[type].data.length ? '' :
|
||||
`<details open data-type="${type}">
|
||||
<summary>${stats[type].label}</summary>` +
|
||||
stats[type].data.map(({text, urls}) => (!urls ? text :
|
||||
`<details open><summary>${text}</summary>${urls.join('')}</details>`
|
||||
)).join('<br>') +
|
||||
'</details>'
|
||||
)).join('') +
|
||||
'</div>');
|
||||
document.querySelector('.regexp-report').onclick = event => {
|
||||
const target = event.target.closest('a, .regexp-report div');
|
||||
if (target) {
|
||||
openURL({url: target.href || target.textContent});
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showHelp(title, text) {
|
||||
var div = document.getElementById("help-popup");
|
||||
div.classList.remove("big");
|
||||
|
@ -1575,14 +1776,16 @@ function showHelp(title, text) {
|
|||
|
||||
if (getComputedStyle(div).display == "none") {
|
||||
document.addEventListener("keydown", closeHelp);
|
||||
div.querySelector(".close-icon").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
|
||||
div.querySelector(".dismiss").onclick = closeHelp; // avoid chaining on multiple showHelp() calls
|
||||
}
|
||||
|
||||
div.style.display = "block";
|
||||
return div;
|
||||
|
||||
function closeHelp(e) {
|
||||
if (e.type == "click" || (e.keyCode == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
|
||||
if (!e
|
||||
|| e.type == "click"
|
||||
|| ((e.keyCode || e.which) == 27 && !e.altKey && !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
|
||||
div.style.display = "";
|
||||
document.querySelector(".contents").innerHTML = "";
|
||||
document.removeEventListener("keydown", closeHelp);
|
||||
|
@ -1625,11 +1828,21 @@ function getParams() {
|
|||
return params;
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(request) {
|
||||
switch (request.method) {
|
||||
case "styleUpdated":
|
||||
if (styleId && styleId == request.id) {
|
||||
initWithStyle(request.style);
|
||||
if (styleId && styleId == request.style.id && request.reason != 'editSave') {
|
||||
if ((request.style.sections[0] || {}).code === null) {
|
||||
// the code-less style came from notifyAllTabs
|
||||
onBackgroundReady().then(() => {
|
||||
request.style = BG.cachedStyles.byId.get(request.style.id);
|
||||
initWithStyle(request);
|
||||
});
|
||||
} else {
|
||||
initWithStyle(request);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "styleDeleted":
|
||||
|
@ -1640,14 +1853,93 @@ chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
|||
}
|
||||
break;
|
||||
case "prefChanged":
|
||||
if (request.prefName == "editor.smartIndent") {
|
||||
CodeMirror.setOption("smartIndent", request.value);
|
||||
if ('editor.smartIndent' in request.prefs) {
|
||||
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
|
||||
}
|
||||
break;
|
||||
case 'editDeleteText':
|
||||
document.execCommand('delete');
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getComputedHeight(el) {
|
||||
var compStyle = getComputedStyle(el);
|
||||
return el.getBoundingClientRect().height +
|
||||
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
|
||||
}
|
||||
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
const themes = Promise.resolve([
|
||||
chrome.i18n.getMessage('defaultTheme'),
|
||||
'3024-day',
|
||||
'3024-night',
|
||||
'abcdef',
|
||||
'ambiance',
|
||||
'ambiance-mobile',
|
||||
'base16-dark',
|
||||
'base16-light',
|
||||
'bespin',
|
||||
'blackboard',
|
||||
'cobalt',
|
||||
'colorforth',
|
||||
'dracula',
|
||||
'duotone-dark',
|
||||
'duotone-light',
|
||||
'eclipse',
|
||||
'elegant',
|
||||
'erlang-dark',
|
||||
'hopscotch',
|
||||
'icecoder',
|
||||
'isotope',
|
||||
'lesser-dark',
|
||||
'liquibyte',
|
||||
'material',
|
||||
'mbo',
|
||||
'mdn-like',
|
||||
'midnight',
|
||||
'monokai',
|
||||
'neat',
|
||||
'neo',
|
||||
'night',
|
||||
'panda-syntax',
|
||||
'paraiso-dark',
|
||||
'paraiso-light',
|
||||
'pastel-on-dark',
|
||||
'railscasts',
|
||||
'rubyblue',
|
||||
'seti',
|
||||
'solarized',
|
||||
'the-matrix',
|
||||
'tomorrow-night-bright',
|
||||
'tomorrow-night-eighties',
|
||||
'ttcn',
|
||||
'twilight',
|
||||
'vibrant-ink',
|
||||
'xq-dark',
|
||||
'xq-light',
|
||||
'yeti',
|
||||
'zenburn',
|
||||
]);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
resolve(themes);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
11
health.js
|
@ -1,11 +0,0 @@
|
|||
healthCheck();
|
||||
|
||||
function healthCheck() {
|
||||
chrome.runtime.sendMessage({method: "healthCheck"}, function(ok) {
|
||||
if (ok === undefined) { // Chrome is starting up
|
||||
healthCheck();
|
||||
} else if (!ok && confirm(t("dbError"))) {
|
||||
window.open("http://userstyles.org/dberror");
|
||||
}
|
||||
});
|
||||
}
|
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 532 B |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 476 B After Width: | Height: | Size: 476 B |
Before Width: | Height: | Size: 331 B After Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 449 B |
Before Width: | Height: | Size: 787 B After Width: | Height: | Size: 787 B |
Before Width: | Height: | Size: 571 B After Width: | Height: | Size: 571 B |
Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 837 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1010 B After Width: | Height: | Size: 1010 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
images/world_go.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
348
install.js
|
@ -1,180 +1,212 @@
|
|||
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) {
|
||||
if (response.length == 0) {
|
||||
sendEvent("styleCanBeInstalledChrome");
|
||||
} else {
|
||||
var installedStyle = response[0];
|
||||
// maybe an update is needed
|
||||
// use the md5 if available
|
||||
var md5Url = getMeta("stylish-md5-url");
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
getResource(md5Url, function(md5) {
|
||||
if (md5 == installedStyle.originalMd5) {
|
||||
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
|
||||
} else {
|
||||
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getResource(getMeta("stylish-code-chrome"), function(code) {
|
||||
// this would indicate a failure (a style with settings?).
|
||||
if (code == null) {
|
||||
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
|
||||
}
|
||||
var json = JSON.parse(code);
|
||||
if (json.sections.length == installedStyle.sections.length) {
|
||||
if (json.sections.every(function(section) {
|
||||
return installedStyle.sections.some(function(installedSection) {
|
||||
return sectionsAreEqual(section, installedSection);
|
||||
});
|
||||
})) {
|
||||
// everything's the same
|
||||
sendEvent("styleAlreadyInstalledChrome", {updateUrl: installedStyle.updateUrl});
|
||||
return;
|
||||
};
|
||||
}
|
||||
sendEvent("styleCanBeUpdatedChrome", {updateUrl: installedStyle.updateUrl});
|
||||
});
|
||||
}
|
||||
}
|
||||
'use strict';
|
||||
|
||||
document.addEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.addEventListener('stylishInstallChrome', onInstallClicked);
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
// orphaned content script check
|
||||
if (msg.method == 'ping') {
|
||||
sendResponse(true);
|
||||
}
|
||||
});
|
||||
|
||||
function sectionsAreEqual(a, b) {
|
||||
if (a.code != b.code) {
|
||||
return false;
|
||||
}
|
||||
return ["urls", "urlPrefixes", "domains", "regexps"].every(function(attribute) {
|
||||
return arraysAreEqual(a[attribute], b[attribute]);
|
||||
});
|
||||
new MutationObserver((mutations, observer) => {
|
||||
if (document.body) {
|
||||
observer.disconnect();
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href
|
||||
}, checkUpdatability);
|
||||
}
|
||||
}).observe(document.documentElement, {childList: true});
|
||||
|
||||
|
||||
function checkUpdatability([installedStyle]) {
|
||||
if (!installedStyle) {
|
||||
sendEvent('styleCanBeInstalledChrome');
|
||||
return;
|
||||
}
|
||||
const md5Url = getMeta('stylish-md5-url');
|
||||
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
|
||||
getResource(md5Url).then(md5 => {
|
||||
reportUpdatable(md5 != installedStyle.originalMd5);
|
||||
});
|
||||
} else {
|
||||
getResource(getMeta('stylish-code-chrome')).then(code => {
|
||||
reportUpdatable(code === null ||
|
||||
!styleSectionsEqual(JSON.parse(code), installedStyle));
|
||||
});
|
||||
}
|
||||
|
||||
function reportUpdatable(isUpdatable) {
|
||||
sendEvent(
|
||||
isUpdatable
|
||||
? 'styleCanBeUpdatedChrome'
|
||||
: 'styleAlreadyInstalledChrome',
|
||||
{
|
||||
updateUrl: installedStyle.updateUrl
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function arraysAreEqual(a, b) {
|
||||
// treat empty array and undefined as equivalent
|
||||
if (typeof a == "undefined")
|
||||
return (typeof b == "undefined") || (b.length == 0);
|
||||
if (typeof b == "undefined")
|
||||
return (typeof a == "undefined") || (a.length == 0);
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.every(function(entry) {
|
||||
return b.indexOf(entry) != -1;
|
||||
});
|
||||
|
||||
function sendEvent(type, detail = null) {
|
||||
detail = {detail};
|
||||
if (typeof cloneInto != 'undefined') {
|
||||
// Firefox requires explicit cloning, however USO can't process our messages anyway
|
||||
// because USO tries to use a global "event" variable deprecated in Firefox
|
||||
detail = cloneInto(detail, document); // eslint-disable-line no-undef
|
||||
}
|
||||
onDOMready().then(() => {
|
||||
document.dispatchEvent(new CustomEvent(type, detail));
|
||||
});
|
||||
}
|
||||
|
||||
function sendEvent(type, data) {
|
||||
if (typeof data == "undefined") {
|
||||
data = null;
|
||||
}
|
||||
var stylishEvent = new CustomEvent(type, {detail: data});
|
||||
document.dispatchEvent(stylishEvent);
|
||||
|
||||
function onInstallClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
getResource(getMeta('stylish-description'))
|
||||
.then(name => saveStyleCode('styleInstall', name))
|
||||
.then(() => getResource(getMeta('stylish-install-ping-url-chrome')));
|
||||
}
|
||||
|
||||
document.addEventListener("stylishUpdateChrome", stylishUpdateChrome);
|
||||
function stylishInstallChrome() {
|
||||
orphanCheck();
|
||||
getResource(getMeta("stylish-description"), function(name) {
|
||||
if (confirm(chrome.i18n.getMessage('styleInstall', [name]))) {
|
||||
getResource(getMeta("stylish-code-chrome"), function(code) {
|
||||
// check for old style json
|
||||
var json = JSON.parse(code);
|
||||
json.method = "saveStyle";
|
||||
chrome.runtime.sendMessage(json, function(response) {
|
||||
sendEvent("styleInstalledChrome");
|
||||
});
|
||||
});
|
||||
getResource(getMeta("stylish-install-ping-url-chrome"));
|
||||
}
|
||||
});
|
||||
|
||||
function onUpdateClicked() {
|
||||
if (!orphanCheck || !orphanCheck()) {
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
url: getMeta('stylish-id-url') || location.href,
|
||||
}, ([style]) => {
|
||||
saveStyleCode('styleUpdate', style.name, {id: style.id});
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("stylishInstallChrome", stylishInstallChrome);
|
||||
function stylishUpdateChrome() {
|
||||
orphanCheck();
|
||||
chrome.runtime.sendMessage({method: "getStyles", url: getMeta("stylish-id-url") || location.href}, function(response) {
|
||||
var style = response[0];
|
||||
if (confirm(chrome.i18n.getMessage('styleUpdate', [style.name]))) {
|
||||
getResource(getMeta("stylish-code-chrome"), function(code) {
|
||||
var json = JSON.parse(code);
|
||||
json.method = "saveStyle";
|
||||
json.id = style.id;
|
||||
chrome.runtime.sendMessage(json, function() {
|
||||
sendEvent("styleInstalledChrome");
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function saveStyleCode(message, name, addProps) {
|
||||
return new Promise(resolve => {
|
||||
if (!confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
return;
|
||||
}
|
||||
getResource(getMeta('stylish-code-chrome')).then(code => {
|
||||
chrome.runtime.sendMessage(
|
||||
Object.assign(JSON.parse(code), addProps, {
|
||||
method: 'saveStyle',
|
||||
reason: 'update',
|
||||
}),
|
||||
() => sendEvent('styleInstalledChrome')
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getMeta(name) {
|
||||
var e = document.querySelector("link[rel='" + name + "']");
|
||||
return e ? e.getAttribute("href") : null;
|
||||
const e = document.querySelector(`link[rel="${name}"]`);
|
||||
return e ? e.getAttribute('href') : null;
|
||||
}
|
||||
|
||||
function getResource(url, callback) {
|
||||
if (url.indexOf("#") == 0) {
|
||||
if (callback) {
|
||||
callback(document.getElementById(url.substring(1)).innerText);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4 && callback) {
|
||||
if (xhr.status >= 400) {
|
||||
callback(null);
|
||||
} else {
|
||||
callback(xhr.responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (url.length > 2000) {
|
||||
var parts = url.split("?");
|
||||
xhr.open("POST", parts[0], true);
|
||||
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
|
||||
xhr.send(parts[1]);
|
||||
} else {
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function getResource(url) {
|
||||
return new Promise(resolve => {
|
||||
if (url.startsWith('#')) {
|
||||
resolve(document.getElementById(url.slice(1)).textContent);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: 'download', url}, resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* stylish to stylus; https://github.com/schomery/stylish-chrome/issues/12 */
|
||||
(function (es) {
|
||||
es.forEach(e => {
|
||||
[...e.childNodes].filter(n => n.nodeType == 3).forEach(n => {
|
||||
n.nodeValue = n.nodeValue.replace('Stylish', 'Stylus');
|
||||
});
|
||||
});
|
||||
})([
|
||||
...document.querySelectorAll('div[id^="stylish-installed-style-not-installed-"]'),
|
||||
...document.querySelectorAll('div[id^="stylish-installed-style-needs-update-"]')
|
||||
]);
|
||||
|
||||
// orphaned content script check
|
||||
function styleSectionsEqual({sections: a}, {sections: b}) {
|
||||
if (!a || !b) {
|
||||
return undefined;
|
||||
}
|
||||
if (a.length != b.length) {
|
||||
return false;
|
||||
}
|
||||
const checkedInB = [];
|
||||
return a.every(sectionA => b.some(sectionB => {
|
||||
if (!checkedInB.includes(sectionB) && propertiesEqual(sectionA, sectionB)) {
|
||||
checkedInB.push(sectionB);
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
|
||||
function propertiesEqual(secA, secB) {
|
||||
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
|
||||
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a == b);
|
||||
}
|
||||
|
||||
function equalOrEmpty(a, b, telltale, comparator) {
|
||||
const typeA = a && typeof a[telltale] == 'function';
|
||||
const typeB = b && typeof b[telltale] == 'function';
|
||||
return (
|
||||
(a === null || a === undefined || (typeA && !a.length)) &&
|
||||
(b === null || b === undefined || (typeB && !b.length))
|
||||
) || typeA && typeB && a.length == b.length && comparator(a, b);
|
||||
}
|
||||
|
||||
function arrayMirrors(array1, array2) {
|
||||
for (const el of array1) {
|
||||
if (array2.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const el of array2) {
|
||||
if (array1.indexOf(el) < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onDOMready() {
|
||||
if (document.readyState != 'loading') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener('DOMContentLoaded', function _() {
|
||||
document.removeEventListener('DOMContentLoaded', _);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) =>
|
||||
msg.method == 'ping' && sendResponse(true));
|
||||
|
||||
function orphanCheck() {
|
||||
var port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return;
|
||||
}
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach event listeners
|
||||
document.removeEventListener('stylishUpdateChrome', stylishUpdateChrome);
|
||||
document.removeEventListener('stylishInstallChrome', stylishInstallChrome);
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy global functions in this context to free up memory
|
||||
[
|
||||
'arraysAreEqual',
|
||||
'getMeta',
|
||||
'getResource',
|
||||
'orphanCheck',
|
||||
'sectionsAreEqual',
|
||||
'sendEvent',
|
||||
'stylishUpdateChrome',
|
||||
'stylishInstallChrome'
|
||||
].forEach(fn => window[fn] = null);
|
||||
const port = chrome.runtime.connect();
|
||||
if (port) {
|
||||
port.disconnect();
|
||||
return true;
|
||||
}
|
||||
// we're orphaned due to an extension update
|
||||
// we can detach event listeners
|
||||
document.removeEventListener('stylishUpdateChrome', onUpdateClicked);
|
||||
document.removeEventListener('stylishInstallChrome', onInstallClicked);
|
||||
// we can't detach chrome.runtime.onMessage because it's no longer connected internally
|
||||
// we can destroy global functions in this context to free up memory
|
||||
[
|
||||
'checkUpdatability',
|
||||
'getMeta',
|
||||
'getResource',
|
||||
'onDOMready',
|
||||
'onInstallClicked',
|
||||
'onUpdateClicked',
|
||||
'orphanCheck',
|
||||
'saveStyleCode',
|
||||
'sendEvent',
|
||||
'styleSectionsEqual',
|
||||
].forEach(fn => (window[fn] = null));
|
||||
}
|
||||
|
|
171
localization.js
|
@ -1,82 +1,121 @@
|
|||
var template = {};
|
||||
'use strict';
|
||||
|
||||
const template = {};
|
||||
tDocLoader();
|
||||
|
||||
|
||||
function t(key, params) {
|
||||
var s = chrome.i18n.getMessage(key, params)
|
||||
if (s == "") {
|
||||
throw "Missing string '" + key + "'.";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
function o(key) {
|
||||
document.write(t(key));
|
||||
const cache = !params && t.cache[key];
|
||||
const s = cache || chrome.i18n.getMessage(key, params);
|
||||
if (s == '') {
|
||||
throw `Missing string "${key}"`;
|
||||
}
|
||||
if (!params && !cache) {
|
||||
t.cache[key] = s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
function tE(id, key, attr, esc) {
|
||||
if (attr) {
|
||||
document.getElementById(id).setAttribute(attr, t(key));
|
||||
} else if (typeof esc == "undefined" || esc) {
|
||||
document.getElementById(id).appendChild(document.createTextNode(t(key)));
|
||||
} else {
|
||||
document.getElementById(id).innerHTML = t(key);
|
||||
}
|
||||
if (attr) {
|
||||
document.getElementById(id).setAttribute(attr, t(key));
|
||||
} else if (typeof esc == 'undefined' || esc) {
|
||||
document.getElementById(id).appendChild(document.createTextNode(t(key)));
|
||||
} else {
|
||||
document.getElementById(id).innerHTML = t(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function tHTML(html) {
|
||||
var node = document.createElement("div");
|
||||
node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use for an explicit space
|
||||
tNodeList(node.querySelectorAll("*"));
|
||||
var child = node.removeChild(node.firstElementChild);
|
||||
node.remove();
|
||||
return child;
|
||||
const node = document.createElement('div');
|
||||
node.innerHTML = html.replace(/>\s+</g, '><'); // spaces are removed; use for an explicit space
|
||||
if (html.includes('i18n-')) {
|
||||
tNodeList(node.getElementsByTagName('*'));
|
||||
}
|
||||
return node.firstElementChild;
|
||||
}
|
||||
|
||||
|
||||
function tNodeList(nodes) {
|
||||
for (var n = 0; n < nodes.length; n++) {
|
||||
var node = nodes[n];
|
||||
if (node.nodeType != 1) { // not an ELEMENT_NODE
|
||||
continue;
|
||||
}
|
||||
if (node.localName == "template") {
|
||||
tNodeList(node.content.querySelectorAll("*"));
|
||||
template[node.dataset.id] = node.content.firstElementChild;
|
||||
continue;
|
||||
}
|
||||
for (var a = node.attributes.length - 1; a >= 0; a--) {
|
||||
var attr = node.attributes[a];
|
||||
var name = attr.nodeName;
|
||||
if (name.indexOf("i18n-") != 0) {
|
||||
continue;
|
||||
}
|
||||
name = name.substr(5); // "i18n-".length
|
||||
var value = t(attr.value);
|
||||
switch (name) {
|
||||
case "text":
|
||||
node.insertBefore(document.createTextNode(value), node.firstChild);
|
||||
break;
|
||||
case "html":
|
||||
node.insertAdjacentHTML("afterbegin", value);
|
||||
break;
|
||||
default:
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
node.removeAttribute(attr.nodeName);
|
||||
}
|
||||
}
|
||||
const PREFIX = 'i18n-';
|
||||
for (let n = nodes.length; --n >= 0;) {
|
||||
const node = nodes[n];
|
||||
// skip non-ELEMENT_NODE
|
||||
if (node.nodeType != 1) {
|
||||
continue;
|
||||
}
|
||||
if (node.localName == 'template') {
|
||||
const elements = node.content.querySelectorAll('*');
|
||||
tNodeList(elements);
|
||||
template[node.dataset.id] = elements[0];
|
||||
// compress inter-tag whitespace to reduce number of DOM nodes by 25%
|
||||
const walker = document.createTreeWalker(elements[0], NodeFilter.SHOW_TEXT);
|
||||
const toRemove = [];
|
||||
while (walker.nextNode()) {
|
||||
const textNode = walker.currentNode;
|
||||
if (!textNode.nodeValue.trim()) {
|
||||
toRemove.push(textNode);
|
||||
}
|
||||
}
|
||||
toRemove.forEach(el => el.remove());
|
||||
continue;
|
||||
}
|
||||
for (let a = node.attributes.length; --a >= 0;) {
|
||||
const attr = node.attributes[a];
|
||||
const name = attr.nodeName;
|
||||
if (!name.startsWith(PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
const type = name.substr(PREFIX.length);
|
||||
const value = t(attr.value);
|
||||
switch (type) {
|
||||
case 'text':
|
||||
node.insertBefore(document.createTextNode(value), node.firstChild);
|
||||
break;
|
||||
case 'text-append':
|
||||
node.appendChild(document.createTextNode(value));
|
||||
break;
|
||||
case 'html':
|
||||
node.insertAdjacentHTML('afterbegin', value);
|
||||
break;
|
||||
default:
|
||||
node.setAttribute(type, value);
|
||||
}
|
||||
node.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function tDocLoader() {
|
||||
// localize HEAD
|
||||
tNodeList(document.querySelectorAll("*"));
|
||||
t.cache = tryJSONparse(localStorage.L10N) || {};
|
||||
const cacheLength = Object.keys(t.cache).length;
|
||||
// localize HEAD
|
||||
tNodeList(document.getElementsByTagName('*'));
|
||||
|
||||
// localize BODY
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
for (var m = 0; m < mutations.length; m++) {
|
||||
tNodeList(mutations[m].addedNodes);
|
||||
}
|
||||
});
|
||||
observer.observe(document, {subtree: true, childList: true});
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
observer.disconnect();
|
||||
tNodeList(document.querySelectorAll("*"));
|
||||
});
|
||||
// localize BODY
|
||||
const process = mutations => {
|
||||
for (const mutation of mutations) {
|
||||
tNodeList(mutation.addedNodes);
|
||||
}
|
||||
};
|
||||
const observer = new MutationObserver(process);
|
||||
const onLoad = () => {
|
||||
tDocLoader.stop();
|
||||
process(observer.takeRecords());
|
||||
if (cacheLength != Object.keys(t.cache).length) {
|
||||
localStorage.L10N = JSON.stringify(t.cache);
|
||||
}
|
||||
};
|
||||
tDocLoader.start = () => {
|
||||
observer.observe(document, {subtree: true, childList: true});
|
||||
};
|
||||
tDocLoader.stop = () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('DOMContentLoaded', onLoad);
|
||||
};
|
||||
tDocLoader.start();
|
||||
document.addEventListener('DOMContentLoaded', onLoad);
|
||||
}
|
||||
|
|
779
manage.css
Normal file
|
@ -0,0 +1,779 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font: 12px arial, sans-serif;
|
||||
/* Firefox: fill the entire page for drag'n'drop to work */
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
transition: color .5s;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 280px;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 15px;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 50px -18px black;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#header a[href^="edit"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.firefox .chromium-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#installed {
|
||||
position: relative;
|
||||
padding-left: 280px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry {
|
||||
margin: 0;
|
||||
padding: 1.25em 2em;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.entry:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
transition: fill .5s;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.svg-icon:hover {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.svg-icon.info {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
margin-left: .5ex;
|
||||
}
|
||||
|
||||
.homepage {
|
||||
margin-left: 0.1em;
|
||||
margin-right: 0.1em;
|
||||
}
|
||||
|
||||
.homepage[href=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.homepage .svg-icon {
|
||||
margin-top: -4px;
|
||||
margin-left: .5ex;
|
||||
}
|
||||
|
||||
.style-name {
|
||||
margin-top: .25em;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.style-name a, .style-edit-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.style-name-link:hover {
|
||||
text-decoration: underline;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.applies-to {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.applies-to,
|
||||
.actions {
|
||||
padding-left: 15px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions > * {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.actions > *:not(:last-child) {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
|
||||
.applies-to label {
|
||||
margin-right: .5ex;
|
||||
}
|
||||
|
||||
.applies-to .target:hover {
|
||||
background-color: rgba(128, 128, 128, .15);
|
||||
}
|
||||
|
||||
.applies-to-extra:not([open]) {
|
||||
display: inline;
|
||||
margin-left: 1ex;
|
||||
}
|
||||
|
||||
summary {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.applies-to-extra summary {
|
||||
list-style-type: none; /* for FF, allegedly */
|
||||
}
|
||||
|
||||
.applies-to-extra summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.disabled h2::after {
|
||||
content: "__MSG_genericDisabledLabel__";
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
text-transform: lowercase;
|
||||
background: rgba(128, 128, 128, .2);
|
||||
padding: 2px 5px 3px;
|
||||
border-radius: 4px;
|
||||
margin-left: 1ex;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.disabled .disable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.enabled .enable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* compact layout */
|
||||
|
||||
.newUI #installed {
|
||||
display: table;
|
||||
margin-top: .75rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
|
||||
.newUI .disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.newUI .disabled .style-name,
|
||||
.newUI .disabled .applies-to {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.newUI .entry {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.newUI .entry:nth-child(2n) {
|
||||
background-color: rgba(128, 128, 128, 0.05);
|
||||
}
|
||||
|
||||
.newUI .entry > * {
|
||||
padding: .9rem 0 1rem;
|
||||
margin: 0;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.newUI .checker {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
.newUI .style-name {
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
text-indent: -2em;
|
||||
padding-left: 3em;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.newUI .homepage .svg-icon {
|
||||
position: absolute;
|
||||
margin-top: 0;
|
||||
margin-left: -28px;
|
||||
}
|
||||
|
||||
.newUI .actions {
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.newUI .actions > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.newUI .actions .svg-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.newUI .updater-icons > * {
|
||||
transition: opacity 1s;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI .entry .svg-icon {
|
||||
fill: #999;
|
||||
}
|
||||
|
||||
.newUI .entry:hover .svg-icon {
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
.newUI .entry:hover .svg-icon:hover {
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
.newUI .checking-update .check-update {
|
||||
opacity: 0;
|
||||
display: inline;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.newUI .can-update .update,
|
||||
.newUI .no-update:not(.update-problem):not(.update-done) .up-to-date,
|
||||
.newUI .no-update.update-problem .check-update,
|
||||
.newUI .update-done .updated {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.newUI .update-done .updated svg {
|
||||
top: -4px;
|
||||
position: relative;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: drop-shadow(0 4px 0 currentColor);
|
||||
filter: drop-shadow(0 5px 0 currentColor);
|
||||
}
|
||||
|
||||
.newUI .can-update .update,
|
||||
.newUI .no-update.update-problem .check-update {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.newUI .can-update[data-details$="locally edited"] .update svg,
|
||||
.newUI .update-problem .check-update svg {
|
||||
fill: #ef6969;
|
||||
}
|
||||
|
||||
.newUI .can-update[data-details$="locally edited"]:hover .update svg,
|
||||
.newUI .entry.update-problem:hover .check-update svg {
|
||||
fill: #fd4040;
|
||||
}
|
||||
|
||||
.newUI .can-update[data-details$="locally edited"]:hover .update svg:hover,
|
||||
.newUI .entry.update-problem:hover .check-update svg:hover {
|
||||
fill: red;
|
||||
}
|
||||
|
||||
.newUI .updater-icons > :not(.check-update):after {
|
||||
content: attr(title);
|
||||
position: absolute;
|
||||
margin-top: 18px;
|
||||
margin-left: -36px;
|
||||
padding: 1ex 1.5ex;
|
||||
border: 1px solid #ded597;
|
||||
background-color: #fffbd6;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 3px 10px rgba(0,0,0,.25);
|
||||
font-size: 90%;
|
||||
animation: fadeout 10s;
|
||||
animation-fill-mode: both;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.newUI .update-problem .check-update:after {
|
||||
background-color: red;
|
||||
border: 1px solid #d40000;
|
||||
color: white;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.newUI .can-update .update:after {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.newUI .can-update:not([data-details$="locally edited"]) .update:after {
|
||||
background-color: #c0fff0;
|
||||
border: 1px solid #89cac9;
|
||||
}
|
||||
|
||||
.newUI .applies-to {
|
||||
padding-top: .25rem;
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
|
||||
.newUI .targets {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.newUI .applies-to.expanded .targets {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.newUI .target {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: calc(100vw - 280px - 60px - 25vw - 3rem);
|
||||
box-sizing: border-box;
|
||||
padding-right: 1rem;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.newUI .applies-to .expander {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 3ex;
|
||||
line-height: .5ex;
|
||||
vertical-align: super;
|
||||
letter-spacing: .1ex;
|
||||
}
|
||||
|
||||
.newUI .applies-to:not(.has-more) .expander {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI .has-favicons .applies-to .expander {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.newUI .target:hover {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.newUI .target img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: sub;
|
||||
margin-left: -20px;
|
||||
margin-right: 4px;
|
||||
transition: opacity .5s, filter .5s;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
|
||||
backface-visibility: hidden;
|
||||
opacity: .25;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI .has-favicons .target {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.newUI .has-favicons .target img[src] {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.newUI .entry:hover .target img {
|
||||
opacity: 1;
|
||||
/* unprefixed since Chrome 53 */
|
||||
-webkit-filter: grayscale(0);
|
||||
filter: grayscale(0);
|
||||
}
|
||||
|
||||
#newUIoptions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI #newUIoptions {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
#newUIoptions > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#newUIoptions input[type="number"] {
|
||||
width: 3em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
input[id^="manage.newUI"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
#faviconsHelp {
|
||||
overflow-y: auto;
|
||||
font-size: 90%;
|
||||
padding: 1ex 0 2ex 16px;
|
||||
}
|
||||
|
||||
#faviconsHelp div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 1ex;
|
||||
}
|
||||
|
||||
/* Default, no update buttons */
|
||||
.update,
|
||||
.check-update {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Check update button for things that can*/
|
||||
.updatable .check-update {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* Update check in progress */
|
||||
.checking-update .check-update {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Updates available */
|
||||
.can-update .update {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.can-update[data-details$="locally edited"] button.update:after {
|
||||
content: "*";
|
||||
}
|
||||
|
||||
.can-update .check-update {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Updates not available */
|
||||
.no-update:not(.update-problem) .check-update {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Updates done */
|
||||
.update-done .check-update {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#apply-all-updates:after {
|
||||
content: " (" attr(data-value) ")";
|
||||
}
|
||||
|
||||
.update-in-progress #check-all-updates {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.update-in-progress #update-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: currentColor;
|
||||
content: "";
|
||||
opacity: .35;
|
||||
}
|
||||
|
||||
#update-all-no-updates[data-skipped-edited="true"]:after {
|
||||
content: " __MSG_updateAllCheckSucceededSomeEdited__";
|
||||
}
|
||||
|
||||
#check-all-updates-force {
|
||||
margin-top: 1ex;
|
||||
}
|
||||
|
||||
/* highlight updated/added styles */
|
||||
.highlight {
|
||||
animation: highlight 10s cubic-bezier(0,.82,.47,.98);
|
||||
}
|
||||
|
||||
@keyframes highlight {
|
||||
from {
|
||||
background-color: rgba(128, 128, 128, .5);
|
||||
}
|
||||
to {
|
||||
background-color: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
fieldset > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#search {
|
||||
width: calc(100% - 4px);
|
||||
margin: 0.25rem 4px 0;
|
||||
border-radius: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
#import ul {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
#import li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
#import pre {
|
||||
background: #eee;
|
||||
overflow: auto;
|
||||
margin: 0 0 .5em 0;
|
||||
}
|
||||
|
||||
/* drag-n-drop on import button */
|
||||
.dropzone:after {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
padding: calc(50vh - 3em) calc(50vw - 5em);
|
||||
content: attr(dragndrop-hint);
|
||||
text-shadow: 1px 1px 10px black;
|
||||
font-size: xx-large;
|
||||
text-align: center;
|
||||
animation: fadein 1s cubic-bezier(.03, .67, .08, .94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.fadeout.dropzone:after {
|
||||
animation: fadeout .25s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
/* post-import report */
|
||||
#message-box details:not([data-id="invalid"]) div:hover {
|
||||
background-color: rgba(128, 128, 128, .3);
|
||||
}
|
||||
|
||||
#message-box details:not(:last-child) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#message-box details small div {
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
.update-history-log {
|
||||
font-size: 11px;
|
||||
white-space: pre;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein-25pct {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: .25;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 675px) {
|
||||
#header {
|
||||
height: auto;
|
||||
position: static;
|
||||
width: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed #AAA;
|
||||
}
|
||||
|
||||
#installed {
|
||||
position: static;
|
||||
padding-left: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
#header h1,
|
||||
#header h2,
|
||||
#header h3,
|
||||
#backup-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#header p,
|
||||
#header fieldset div,
|
||||
#backup {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#find-editor-styles {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#backup {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
#backup p,
|
||||
#header fieldset {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.entry {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.newUI #header {
|
||||
height: auto;
|
||||
position: static;
|
||||
width: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px dashed #AAA;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.newUI #installed {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.newUI #header h1,
|
||||
.newUI #header h2,
|
||||
.newUI #header h3,
|
||||
.newUI #header legend,
|
||||
.newUI #backup-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.newUI #header p,
|
||||
.newUI #header fieldset div,
|
||||
.newUI #options,
|
||||
.newUI #backup,
|
||||
.newUI #find-editor-styles,
|
||||
.newUI #header fieldset label,
|
||||
.newUI #header fieldset input,
|
||||
.newUI #newUIoptions > * {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
.newUI #header > * {
|
||||
display: inline-block
|
||||
}
|
||||
|
||||
.newUI #header button,
|
||||
.newUI #header span,
|
||||
.newUI #header div {
|
||||
margin-right: 1ex;
|
||||
}
|
||||
|
||||
.newUI #header label,
|
||||
.newUI #header a {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.newUI #backup p,
|
||||
.newUI #header fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.newUI #header fieldset input {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.newUI #search {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.newUI .entry {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.newUI .style-name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.newUI .target {
|
||||
max-width: calc(50vw - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.newUI #header > * {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.newUI .style-name {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
495
manage.html
|
@ -1,300 +1,223 @@
|
|||
<html>
|
||||
<html id="stylus">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<title i18n-text="manageTitle"></title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font: 12px arial, sans-serif;
|
||||
}
|
||||
a,
|
||||
a:visited {
|
||||
color: #555;
|
||||
-webkit-transition: color 0.5s;
|
||||
}
|
||||
a:hover {
|
||||
color: #999;
|
||||
}
|
||||
#header {
|
||||
height: 100%;
|
||||
width: 250px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
padding: 15px;
|
||||
border-right: 1px dashed #AAA;
|
||||
-webkit-box-shadow: 0 0 50px -18px black;
|
||||
}
|
||||
#header h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
#installed {
|
||||
position: relative;
|
||||
margin-left: 280px;
|
||||
}
|
||||
[style-id] {
|
||||
margin: 10px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
[style-id] {
|
||||
border-top: 2px solid gray;
|
||||
}
|
||||
#installed::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: #fff;
|
||||
}
|
||||
.svg-icon.installed {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.3rem;
|
||||
margin-top: -4px;
|
||||
transition: fill .5s;
|
||||
}
|
||||
a:hover .svg-icon.installed {
|
||||
fill: hsl(0, 0%, 40%);
|
||||
}
|
||||
.style-name {
|
||||
margin-top: .25em;
|
||||
word-break: break-word;
|
||||
}
|
||||
.applies-to {
|
||||
word-break: break-word;
|
||||
}
|
||||
.applies-to,
|
||||
.actions {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.applies-to-extra {
|
||||
font-weight: bold;
|
||||
}
|
||||
.disabled h2::after {
|
||||
content: " (Disabled)";
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.disabled .disable {
|
||||
display: none;
|
||||
}
|
||||
.enabled .enable {
|
||||
display: none;
|
||||
}
|
||||
.style-name a[target="_blank"] {
|
||||
text-decoration: none;
|
||||
}
|
||||
/* Default, no update buttons */
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<title i18n-text="manageTitle"></title>
|
||||
<link rel="stylesheet" href="manage.css">
|
||||
<link rel="stylesheet" href="msgbox/msgbox.css">
|
||||
|
||||
.update,
|
||||
.check-update {
|
||||
display: none;
|
||||
}
|
||||
/* Check update button for things that can*/
|
||||
<style id="style-overrides"></style>
|
||||
|
||||
*[style-update-url] .check-update {
|
||||
display: inline;
|
||||
}
|
||||
/* Update check in progress */
|
||||
<!-- Notes:
|
||||
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
|
||||
* inter-tag whitespace in templates is automatically removed in localization.js
|
||||
* i18n-anything attribute automatically creates "anything" attribute
|
||||
-->
|
||||
|
||||
.checking-update .check-update {
|
||||
display: none;
|
||||
}
|
||||
/* Updates available */
|
||||
|
||||
.can-update .update {
|
||||
display: inline;
|
||||
}
|
||||
.can-update .check-update {
|
||||
display: none;
|
||||
}
|
||||
/* Updates not available */
|
||||
|
||||
.no-update .check-update {
|
||||
display: none;
|
||||
}
|
||||
/* Updates done */
|
||||
|
||||
.update-done .check-update {
|
||||
display: none;
|
||||
}
|
||||
.hidden {
|
||||
display: none
|
||||
}
|
||||
@media(max-width:675px) {
|
||||
#header {
|
||||
height: auto;
|
||||
position: inherit;
|
||||
width: auto;
|
||||
border-right: none;
|
||||
}
|
||||
#installed {
|
||||
margin-left: 0;
|
||||
}
|
||||
[style-id] {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
#header {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 30px)
|
||||
}
|
||||
fieldset {
|
||||
border-width: 1px;
|
||||
border-radius: 6px;
|
||||
margin: 1em 0;
|
||||
}
|
||||
.enabled-only > .disabled,
|
||||
.edited-only > [style-update-url] {
|
||||
display: none;
|
||||
}
|
||||
#search {
|
||||
width: calc(100% - 4px);
|
||||
margin: 0.25rem 4px 0;
|
||||
border-radius: 0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
border-width: 1px;
|
||||
}
|
||||
#import ul {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
list-style: none;
|
||||
}
|
||||
#import li {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
#import pre {
|
||||
background: #eee;
|
||||
overflow: auto;
|
||||
margin: 0 0 .5em 0;
|
||||
}
|
||||
/* drag-n-drop on import button */
|
||||
.dropzone:after {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
padding: calc(50vh - 3em) calc(50vw - 5em);
|
||||
content: attr(dragndrop-hint);
|
||||
text-shadow: 1px 1px 10px black;
|
||||
font-size: xx-large;
|
||||
text-align: center;
|
||||
animation: fadein 1s cubic-bezier(.03,.67,.08,.94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
.fadeout.dropzone:after {
|
||||
animation: fadeout .25s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes fadeout {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<template data-id="style">
|
||||
<div>
|
||||
<h2 class="style-name"></h2>
|
||||
<p class="applies-to"></p>
|
||||
<p class="actions">
|
||||
<a class="style-edit-link" href="edit.html?id="><button i18n-text="editStyleLabel"></button></a>
|
||||
<button class="enable" i18n-text="enableStyleLabel"></button>
|
||||
<button class="disable" i18n-text="disableStyleLabel"></button>
|
||||
<button class="delete" i18n-text="deleteStyleLabel"></button>
|
||||
<button class="check-update" i18n-text="checkForUpdate"></button>
|
||||
<button class="update" i18n-text="installUpdate"></button>
|
||||
<span class="update-note"></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="styleHomepage">
|
||||
<a target="_blank">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#000000" class="svg-icon installed" height="16" width="16" viewBox="0 0 8 8">
|
||||
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z" id="external-link"></path>
|
||||
</svg>
|
||||
<template data-id="style">
|
||||
<div class="entry">
|
||||
<h2 class="style-name">
|
||||
<a class="style-name-link" href="edit.html?id="></a>
|
||||
<a target="_blank" class="homepage"></a>
|
||||
</h2>
|
||||
<p class="applies-to">
|
||||
<label i18n-text="appliesDisplay"></label>
|
||||
<span class="targets"></span>
|
||||
</p>
|
||||
<p class="actions">
|
||||
<a class="style-edit-link" href="edit.html?id=">
|
||||
<button i18n-text="editStyleLabel"></button>
|
||||
</a>
|
||||
</template>
|
||||
<button class="enable" i18n-text="enableStyleLabel"></button>
|
||||
<button class="disable" i18n-text="disableStyleLabel"></button>
|
||||
<button class="delete" i18n-text="deleteStyleLabel"></button>
|
||||
<button class="check-update" i18n-text="checkForUpdate"></button>
|
||||
<button class="update" i18n-text="installUpdate"></button>
|
||||
<span class="update-note"></span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="localization.js"></script>
|
||||
<script src="health.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="manage.js"></script>
|
||||
<template data-id="styleCompact">
|
||||
<div class="entry">
|
||||
<h2 class="style-name">
|
||||
<input class="checker" type="checkbox" i18n-title="toggleStyle">
|
||||
<a class="style-name-link" href="edit.html?id="></a>
|
||||
</h2>
|
||||
<p class="actions">
|
||||
<a target="_blank" class="homepage"></a>
|
||||
<span i18n-title="deleteStyleLabel">
|
||||
<svg class="svg-icon delete" viewBox="0 0 20 20">
|
||||
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
|
||||
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
|
||||
</svg>
|
||||
</span>
|
||||
</p>
|
||||
<div class="applies-to">
|
||||
<div class="targets"></div>
|
||||
<span class="expander">...</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template data-id="homepageIconBig">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon shape-rendering="crispEdges" points="3,3 3,17 17,17 17,13 15,13 15,15 5,15 5,5 7,5 7,3 "/>
|
||||
<polygon points="10,3 12.5,5.5 8,10 10,12 14.5,7.5 17,10 17,3 "/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template data-id="homepageIconSmall">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M4,4h5v2H6v8h8v-3h2v5H4V4z M11,3h6v6l-2-2l-4,4L9,9l4-4L11,3z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template data-id="updaterIcons">
|
||||
<span class="updater-icons">
|
||||
<span class="check-update" i18n-title="checkForUpdate">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<path d="M18,16.6l-3.1-3.1c0.5-0.7,0.9-1.5,1-2.5h-2.1c-0.4,1.7-2,3-3.9,3c-0.8,0-1.6-0.3-2.3-0.7
|
||||
L10,11H6.1H4.1H4v6l2.3-2.3c1,0.8,2.3,1.3,3.7,1.3c1.3,0,2.5-0.4,3.5-1.1l3.1,3.1L18,16.6z"/>
|
||||
<path d="M10,6c0.8,0,1.6,0.3,2.3,0.7L10,9h3.9h2.1H16V3l-2.3,2.3C12.7,4.5,11.4,4,10,4
|
||||
C7,4,4.6,6.2,4.1,9h2.1C6.6,7.3,8.1,6,10,6z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="update" i18n-title="installUpdate">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="16,8 12,8 12,3 8,3 8,8 4,8 10,14 "/>
|
||||
<rect shape-rendering="crispEdges" x="4" y="15" width="12" height="2"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="up-to-date" i18n-title="updateCheckSucceededNoUpdate">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="updated" i18n-title="updateCompleted">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20">
|
||||
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="update-note"></span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToTarget">
|
||||
<span class="target"></span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToSeparator">
|
||||
<span class="sep">, </span>
|
||||
</template>
|
||||
|
||||
<template data-id="appliesToEverything">
|
||||
<span class="target" i18n-text="appliesToEverything"></span>
|
||||
</template>
|
||||
|
||||
<template data-id="extraAppliesTo">
|
||||
<details class="applies-to-extra">
|
||||
<summary i18n-html="appliesDisplayTruncatedSuffix"></summary>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="manage.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-manage" i18n-dragndrop-hint="dragDropMessage">
|
||||
<div id="header">
|
||||
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
|
||||
<fieldset>
|
||||
<legend id="filters" i18n-text="manageFilters"></legend>
|
||||
<div>
|
||||
<input id="manage.onlyEnabled" type="checkbox">
|
||||
<label id="manage.onlyEnabled-label" for="manage.onlyEnabled" i18n-text="manageOnlyEnabled"></label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="manage.onlyEdited" type="checkbox">
|
||||
<label id="manage.onlyEdited-label" for="manage.onlyEdited" i18n-text="manageOnlyEdited"></label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles">
|
||||
</div>
|
||||
</fieldset>
|
||||
<p>
|
||||
<button id="check-all-updates" i18n-text="checkAllUpdates"></button>
|
||||
</p>
|
||||
<p>
|
||||
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
|
||||
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
|
||||
</p>
|
||||
<p>
|
||||
<a href="edit.html">
|
||||
<button id="add-style-label" i18n-text="addStyleLabel"></button>
|
||||
</a>
|
||||
</p>
|
||||
<div id="options">
|
||||
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
|
||||
<div>
|
||||
<input id="show-badge" type="checkbox">
|
||||
<label id="show-badge-label" for="show-badge" i18n-text="prefShowBadge"></label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="popup.stylesFirst" type="checkbox">
|
||||
<label id="stylesFirst-label" for="popup.stylesFirst" i18n-text="popupStylesFirst"></label>
|
||||
</div>
|
||||
<div id="more-options">
|
||||
<h3 id="options-subheading" i18n-text="optionsSubheading"></h3>
|
||||
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
|
||||
<button id="manage-shortcuts-button" i18n-text="openOptionsShortcuts"></button>
|
||||
<p>
|
||||
<button id="editor-styles-button" i18n-text="editorStylesButton"></button>
|
||||
</p>
|
||||
</div>
|
||||
<div id="header">
|
||||
<h1 id="manage-heading" i18n-text="manageHeading"></h1>
|
||||
<fieldset>
|
||||
<legend id="filters" i18n-text="manageFilters"></legend>
|
||||
<label>
|
||||
<input id="manage.onlyEnabled" type="checkbox"
|
||||
data-filter=".enabled"
|
||||
data-filter-hide=".disabled">
|
||||
<span i18n-text="manageOnlyEnabled"></span>
|
||||
</label>
|
||||
<label>
|
||||
<input id="manage.onlyLocal" type="checkbox"
|
||||
data-filter=":not(.updatable)"
|
||||
data-filter-hide=".updatable">
|
||||
<span i18n-text="manageOnlyLocal" i18n-title="manageOnlyLocalTooltip"></span>
|
||||
</label>
|
||||
<label id="onlyUpdates" class="hidden">
|
||||
<input type="checkbox"
|
||||
data-filter=".can-update, .update-problem, .update-done"
|
||||
data-filter-hide=":not(.updatable):not(.update-done), .no-update:not(.update-problem)">
|
||||
<span i18n-text="manageOnlyUpdates"></span>
|
||||
</label>
|
||||
<input id="search" type="search" i18n-placeholder="searchStyles"
|
||||
data-filter=":not(.not-matching)"
|
||||
data-filter-hide=".not-matching">
|
||||
</fieldset>
|
||||
<p>
|
||||
<button id="check-all-updates" i18n-text="checkAllUpdates"><span id="update-progress"></span></button>
|
||||
<span id="update-history" i18n-title="genericHistoryLabel">
|
||||
<svg class="svg-icon" viewBox="0 0 20 20" i18n-alt="helpAlt">
|
||||
<path d="M13,7H7V6h6Zm6,6.5A5.5,5.5,0,0,1,8.61,16H4V3H16V8.61A5.5,5.5,0,0,1,19,13.5ZM8,14c0-.16,0-.84,0-1H7V12H8.21a5.46,5.46,0,0,1,.39-1H7V10H9.26a5.55,5.55,0,0,1,1.09-1H7V8h7V5H6v9Zm10-.5A4.5,4.5,0,1,0,13.5,18,4.5,4.5,0,0,0,18,13.5ZM14,13V10H13v4h4V13Z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<button id="apply-all-updates" class="hidden" i18n-text="applyAllUpdates"></button>
|
||||
<span id="update-all-no-updates" class="hidden" i18n-text="updateAllCheckSucceededNoUpdate"></span>
|
||||
<button id="check-all-updates-force" class="hidden" i18n-text="checkAllUpdatesForce"></button>
|
||||
</p>
|
||||
<p>
|
||||
<a href="edit.html">
|
||||
<button id="add-style-label" i18n-text="addStyleLabel"></button>
|
||||
</a>
|
||||
</p>
|
||||
<div id="options">
|
||||
<h2 id="options-heading" i18n-text="optionsHeading"></h2>
|
||||
<label><input id="manage.newUI" type="checkbox"><span i18n-text="manageNewUI"></span></label>
|
||||
<div id="newUIoptions">
|
||||
<div>
|
||||
<input id="manage.newUI.favicons" type="checkbox">
|
||||
<label for="manage.newUI.favicons" i18n-text="manageFavicons"></label>
|
||||
<svg class="svg-icon info" viewBox="0 0 14 16" i18n-alt="helpAlt" data-toggle-on-click="#faviconsHelp">
|
||||
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
|
||||
</svg>
|
||||
<div id="faviconsHelp" class="hidden" i18n-text="manageFaviconsHelp">
|
||||
<div>
|
||||
<input id="manage.newUI.faviconsGray" type="checkbox">
|
||||
<label for="manage.newUI.faviconsGray" i18n-text="manageFaviconsGray"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backup">
|
||||
<h2 id="backup-title" i18n-text="backupButtons"></h2>
|
||||
<span id="backup-message" i18n-text="backupMessage"></span>
|
||||
<p>
|
||||
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
|
||||
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
|
||||
</p>
|
||||
</div>
|
||||
<p id="manage-text" i18n-html="manageText"></p>
|
||||
</div>
|
||||
<label><input id="manage.newUI.targets" type="number" min="1" max="99"><span i18n-text="manageMaxTargets"></span></label>
|
||||
</div>
|
||||
<div id="installed"></div>
|
||||
<script src="openOptions.js"></script>
|
||||
<script src="backup/fileSaveLoad.js"></script>
|
||||
</body>
|
||||
<p>
|
||||
<button id="manage-options-button" i18n-text="openOptionsManage"></button>
|
||||
<button id="manage-shortcuts-button" class="chromium-only"
|
||||
i18n-text="shortcuts"
|
||||
i18n-title="shortcutsNote"></button>
|
||||
<a id="find-editor-styles"
|
||||
href="https://userstyles.org/styles/browse/chrome-extension"
|
||||
i18n-title="editorStylesButton"
|
||||
target="_blank"><button i18n-text="cm_theme"></button></a>
|
||||
</p>
|
||||
</div>
|
||||
<div id="backup">
|
||||
<h2 id="backup-title" i18n-text="backupButtons"></h2>
|
||||
<span id="backup-message" i18n-text="backupMessage"></span>
|
||||
<p>
|
||||
<button id="file-all-styles" i18n-text="bckpInstStyles"></button>
|
||||
<button id="unfile-all-styles" i18n-text="retrieveBckp"></button>
|
||||
</p>
|
||||
</div>
|
||||
<p id="manage-text" i18n-html="manageText"></p>
|
||||
</div>
|
||||
<div id="installed"></div>
|
||||
|
||||
<script src="backup/fileSaveLoad.js"></script>
|
||||
<script src="msgbox/msgbox.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
{
|
||||
"name": "Stylus",
|
||||
"version": "1.0.5",
|
||||
"minimum_chrome_version": "49",
|
||||
"description": "__MSG_description__",
|
||||
"homepage_url": "http://add0n.com/stylus.html",
|
||||
"manifest_version": 2,
|
||||
"icons": {
|
||||
"16": "16.png",
|
||||
"48": "48.png",
|
||||
"128": "128.png"
|
||||
"16": "/images/icon/16.png",
|
||||
"32": "/images/icon/32.png",
|
||||
"48": "/images/icon/48.png",
|
||||
"128": "/images/icon/128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"contextMenus",
|
||||
"storage",
|
||||
"*://*/*"
|
||||
"<all_urls>"
|
||||
],
|
||||
"background": {
|
||||
"scripts": ["messaging.js", "storage-websql.js", "storage.js", "background.js", "update.js"]
|
||||
"scripts": ["messaging.js", "storage.js", "prefs.js", "background.js", "update.js"]
|
||||
},
|
||||
"commands": {
|
||||
"openManage": {
|
||||
|
@ -32,21 +34,22 @@
|
|||
"matches": ["<all_urls>"],
|
||||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"match_about_blank": true,
|
||||
"js": ["apply.js"]
|
||||
},
|
||||
{
|
||||
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
|
||||
"run_at": "document_end",
|
||||
"run_at": "document_start",
|
||||
"all_frames": false,
|
||||
"js": ["install.js"]
|
||||
}
|
||||
],
|
||||
"browser_action": {
|
||||
"default_icon": {
|
||||
"16": "16w.png",
|
||||
"32": "32w.png",
|
||||
"19": "19w.png",
|
||||
"38": "38w.png"
|
||||
"16": "/images/icon/16w.png",
|
||||
"32": "/images/icon/32w.png",
|
||||
"19": "/images/icon/19w.png",
|
||||
"38": "/images/icon/38w.png"
|
||||
},
|
||||
"default_title": "Stylus",
|
||||
"default_popup": "popup.html"
|
||||
|
|
432
messaging.js
|
@ -1,146 +1,328 @@
|
|||
/* global BG: true, onRuntimeMessage, applyOnMessage, handleUpdate, handleDelete */
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
const KEEP_CHANNEL_OPEN = true;
|
||||
const OWN_ORIGIN = chrome.runtime.getURL('');
|
||||
|
||||
function notifyAllTabs(request) {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
for (let tab of tabs) {
|
||||
if (request.codeIsUpdated !== false || tab.url.startsWith(OWN_ORIGIN)) {
|
||||
chrome.tabs.sendMessage(tab.id, request);
|
||||
updateIcon(tab);
|
||||
}
|
||||
}
|
||||
});
|
||||
// notify all open popups
|
||||
const reqPopup = Object.assign({}, request, {method: 'updatePopup', reason: request.method});
|
||||
chrome.runtime.sendMessage(reqPopup);
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(reqPopup);
|
||||
}
|
||||
const FIREFOX = /Firefox/.test(navigator.userAgent);
|
||||
const OPERA = /OPR/.test(navigator.userAgent);
|
||||
|
||||
const URLS = {
|
||||
ownOrigin: chrome.runtime.getURL(''),
|
||||
|
||||
optionsUI: [
|
||||
chrome.runtime.getURL('options/index.html'),
|
||||
'chrome://extensions/?options=' + chrome.runtime.id,
|
||||
],
|
||||
|
||||
configureCommands:
|
||||
OPERA ? 'opera://settings/configureCommands'
|
||||
: 'chrome://extensions/configureCommands',
|
||||
|
||||
// CWS cannot be scripted in chromium, see ChromeExtensionsClient::IsScriptableURL
|
||||
// https://cs.chromium.org/chromium/src/chrome/common/extensions/chrome_extensions_client.cc
|
||||
chromeWebStore: FIREFOX ? 'N/A' : 'https://chrome.google.com/webstore/',
|
||||
|
||||
supported: new RegExp(
|
||||
'^(file|ftps?|http)://|' +
|
||||
`^https://${FIREFOX ? '' : '(?!chrome\\.google\\.com/webstore)'}|` +
|
||||
'^' + chrome.runtime.getURL('')),
|
||||
};
|
||||
|
||||
let BG = chrome.extension.getBackgroundPage();
|
||||
|
||||
if (!BG || BG != window) {
|
||||
document.documentElement.classList.toggle('firefox', FIREFOX);
|
||||
document.documentElement.classList.toggle('opera', OPERA);
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
if (navigator.userAgent.includes('Chrome/49.')) {
|
||||
getActiveTab().then(BG.updateIcon);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAllTabs() {
|
||||
return new Promise(resolve => {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query({}, tabs => {
|
||||
const lastTab = tabs[tabs.length - 1];
|
||||
for (let tab of tabs) {
|
||||
getStyles({matchUrl: tab.url, enabled: true, asHash: true}, styles => {
|
||||
const message = {method: 'styleReplaceAll', styles};
|
||||
if (tab.url == location.href && typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(message);
|
||||
} else {
|
||||
chrome.tabs.sendMessage(tab.id, message);
|
||||
}
|
||||
updateIcon(tab, styles);
|
||||
if (tab == lastTab) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
if (msg.method == 'styleUpdated' || msg.method == 'styleAdded') {
|
||||
// apply/popup/manage use only meta for these two methods,
|
||||
// editor may need the full code but can fetch it directly,
|
||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
||||
msg = Object.assign({}, msg, {
|
||||
style: getStyleWithNoCode(msg.style)
|
||||
});
|
||||
}
|
||||
const affectsAll = !msg.affects || msg.affects.all;
|
||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
||||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
const affectsSelf = affectsPopup || msg.prefs;
|
||||
if (affectsTabs || affectsIcon) {
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
chrome.tabs.query(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}, tabs => {
|
||||
for (const tab of tabs) {
|
||||
// own pages will be notified via runtime.sendMessage later
|
||||
if ((affectsTabs || URLS.optionsUI.includes(tab.url))
|
||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))) {
|
||||
chrome.tabs.sendMessage(tab.id, msg);
|
||||
}
|
||||
if (affectsIcon && BG) {
|
||||
BG.updateIcon(tab);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (typeof onRuntimeMessage != 'undefined') {
|
||||
onRuntimeMessage(originalMessage);
|
||||
}
|
||||
// notify apply.js on own pages
|
||||
if (typeof applyOnMessage != 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// notify background page and all open popups
|
||||
if (affectsSelf) {
|
||||
chrome.runtime.sendMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
function updateIcon(tab, styles) {
|
||||
// while NTP is still loading only process the request for its main frame with a real url
|
||||
// (but when it's loaded we should process style toggle requests from popups, for example)
|
||||
if (tab.url == "chrome://newtab/" && tab.status != "complete") {
|
||||
return;
|
||||
}
|
||||
if (styles) {
|
||||
// check for not-yet-existing tabs e.g. omnibox instant search
|
||||
chrome.tabs.get(tab.id, function() {
|
||||
if (!chrome.runtime.lastError) {
|
||||
// for 'styles' asHash:true fake the length by counting numeric ids manually
|
||||
if (styles.length === undefined) {
|
||||
styles.length = 0;
|
||||
for (var id in styles) {
|
||||
styles.length += id.match(/^\d+$/) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
stylesReceived(styles);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
getTabRealURL(tab, function(url) {
|
||||
// if we have access to this, call directly. a page sending a message to itself doesn't seem to work right.
|
||||
if (typeof getStyles != "undefined") {
|
||||
getStyles({matchUrl: url, enabled: true}, stylesReceived);
|
||||
} else {
|
||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url, enabled: true}, stylesReceived);
|
||||
}
|
||||
});
|
||||
|
||||
function stylesReceived(styles) {
|
||||
var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll");
|
||||
var postfix = disableAll ? "x" : styles.length == 0 ? "w" : "";
|
||||
chrome.browserAction.setIcon({
|
||||
path: {
|
||||
// Material Design 2016 new size is 16px
|
||||
16: "16" + postfix + ".png", 32: "32" + postfix + ".png",
|
||||
// Chromium forks or non-chromium browsers may still use the traditional 19px
|
||||
19: "19" + postfix + ".png", 38: "38" + postfix + ".png",
|
||||
},
|
||||
tabId: tab.id
|
||||
}, function() {
|
||||
// if the tab was just closed an error may occur,
|
||||
// e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload
|
||||
if (!chrome.runtime.lastError) {
|
||||
var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : "";
|
||||
chrome.browserAction.setBadgeText({text: t, tabId: tab.id});
|
||||
chrome.browserAction.setBadgeBackgroundColor({
|
||||
color: prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal')
|
||||
});
|
||||
}
|
||||
});
|
||||
//console.log("Tab " + tab.id + " (" + tab.url + ") badge text set to '" + t + "'.");
|
||||
}
|
||||
function getTab(id) {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.get(id, tab =>
|
||||
!chrome.runtime.lastError && resolve(tab)));
|
||||
}
|
||||
|
||||
function getActiveTab(callback) {
|
||||
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
|
||||
callback(tabs[0]);
|
||||
});
|
||||
|
||||
function getActiveTab() {
|
||||
return new Promise(resolve =>
|
||||
chrome.tabs.query({currentWindow: true, active: true}, tabs =>
|
||||
resolve(tabs[0])));
|
||||
}
|
||||
|
||||
function getActiveTabRealURL(callback) {
|
||||
getActiveTab(function(tab) {
|
||||
getTabRealURL(tab, callback);
|
||||
});
|
||||
|
||||
function getActiveTabRealURL() {
|
||||
return getActiveTab()
|
||||
.then(getTabRealURL);
|
||||
}
|
||||
|
||||
function getTabRealURL(tab, callback) {
|
||||
if (tab.url != "chrome://newtab/") {
|
||||
callback(tab.url);
|
||||
} else {
|
||||
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, function(frame) {
|
||||
frame && callback(frame.url);
|
||||
});
|
||||
}
|
||||
|
||||
function getTabRealURL(tab) {
|
||||
return new Promise(resolve => {
|
||||
if (tab.url != 'chrome://newtab/') {
|
||||
resolve(tab.url);
|
||||
} else {
|
||||
chrome.webNavigation.getFrame({tabId: tab.id, frameId: 0, processId: -1}, frame => {
|
||||
resolve(frame && frame.url || '');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// opens a tab or activates the already opened one,
|
||||
// reuses the New Tab page if it's focused now
|
||||
function openURL({url, currentWindow = true}) {
|
||||
if (!url.includes('://')) {
|
||||
url = chrome.runtime.getURL(url);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
// [some] chromium forks don't handle their fake branded protocols
|
||||
url = url.replace(/^(opera|vivaldi)/, 'chrome');
|
||||
// API doesn't handle the hash-fragment part
|
||||
chrome.tabs.query({url: url.replace(/#.*/, ''), currentWindow}, tabs => {
|
||||
for (const tab of tabs) {
|
||||
if (tab.url == url) {
|
||||
activateTab(tab).then(resolve);
|
||||
return;
|
||||
}
|
||||
}
|
||||
getActiveTab().then(tab => {
|
||||
if (tab && tab.url == 'chrome://newtab/') {
|
||||
chrome.tabs.update({url}, resolve);
|
||||
} else {
|
||||
chrome.tabs.create(tab && !FIREFOX ? {url, openerTabId: tab.id} : {url}, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function activateTab(tab) {
|
||||
return Promise.all([
|
||||
new Promise(resolve => {
|
||||
chrome.tabs.update(tab.id, {active: true}, resolve);
|
||||
}),
|
||||
new Promise(resolve => {
|
||||
chrome.windows.update(tab.windowId, {focused: true}, resolve);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
function stringAsRegExp(s, flags) {
|
||||
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=*!|]/g, "\\$&"), flags);
|
||||
return new RegExp(s.replace(/[{}()[\]/\\.+?^$:=*!|]/g, '\\$&'), flags);
|
||||
}
|
||||
|
||||
// expands * as .*?
|
||||
function wildcardAsRegExp(s, flags) {
|
||||
return new RegExp(s.replace(/[{}()\[\]\/\\.+?^$:=!|]/g, "\\$&").replace(/\*/g, '.*?'), flags);
|
||||
|
||||
function ignoreChromeError() {
|
||||
chrome.runtime.lastError; // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
var configureCommands = {
|
||||
get url () {
|
||||
return navigator.userAgent.indexOf('OPR') > -1 ?
|
||||
'opera://settings/configureCommands' :
|
||||
'chrome://extensions/configureCommands'
|
||||
},
|
||||
open: () => {
|
||||
chrome.tabs.create({
|
||||
'url': configureCommands.url
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function getStyleWithNoCode(style) {
|
||||
const stripped = Object.assign({}, style, {sections: []});
|
||||
for (const section of style.sections) {
|
||||
stripped.sections.push(Object.assign({}, section, {code: null}));
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
|
||||
// js engine can't optimize the entire function if it contains try-catch
|
||||
// so we should keep it isolated from normal code in a minimal wrapper
|
||||
// Update: might get fixed in V8 TurboFan in the future
|
||||
function tryCatch(func, ...args) {
|
||||
try {
|
||||
return func(...args);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryRegExp(regexp) {
|
||||
try {
|
||||
return new RegExp(regexp);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
function tryJSONparse(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
||||
const debounce = Object.assign((fn, delay, ...args) => {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.set(fn, setTimeout(debounce.run, delay, fn, ...args));
|
||||
}, {
|
||||
timers: new Map(),
|
||||
run(fn, ...args) {
|
||||
debounce.timers.delete(fn);
|
||||
fn(...args);
|
||||
},
|
||||
unregister(fn) {
|
||||
clearTimeout(debounce.timers.get(fn));
|
||||
debounce.timers.delete(fn);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
function deepCopy(obj) {
|
||||
return obj !== null && obj !== undefined && typeof obj == 'object'
|
||||
? deepMerge(typeof obj.slice == 'function' ? [] : {}, obj)
|
||||
: obj;
|
||||
}
|
||||
|
||||
|
||||
function deepMerge(target, ...args) {
|
||||
const isArray = typeof target.slice == 'function';
|
||||
for (const obj of args) {
|
||||
if (isArray && obj !== null && obj !== undefined) {
|
||||
for (const element of obj) {
|
||||
target.push(deepCopy(element));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for (const k in obj) {
|
||||
const value = obj[k];
|
||||
if (k in target && typeof value == 'object' && value !== null) {
|
||||
deepMerge(target[k], value);
|
||||
} else {
|
||||
target[k] = deepCopy(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
|
||||
function sessionStorageHash(name) {
|
||||
return {
|
||||
name,
|
||||
value: tryCatch(JSON.parse, sessionStorage[name]) || {},
|
||||
set(k, v) {
|
||||
this.value[k] = v;
|
||||
this.updateStorage();
|
||||
},
|
||||
unset(k) {
|
||||
delete this.value[k];
|
||||
this.updateStorage();
|
||||
},
|
||||
updateStorage() {
|
||||
sessionStorage[this.name] = JSON.stringify(this.value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function onBackgroundReady(...dataPassthru) {
|
||||
return BG ? Promise.resolve(...dataPassthru) : new Promise(ping);
|
||||
function ping(resolve) {
|
||||
chrome.runtime.sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve(...dataPassthru);
|
||||
} else {
|
||||
ping(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// in case Chrome haven't yet loaded the bg page and displays our page like edit/manage
|
||||
function getStylesSafe(options) {
|
||||
return onBackgroundReady(options).then(BG.getStyles);
|
||||
}
|
||||
|
||||
|
||||
function saveStyleSafe(style) {
|
||||
return onBackgroundReady(BG.deepCopy(style))
|
||||
.then(BG.saveStyle)
|
||||
.then(savedStyle => {
|
||||
if (style.notify === false) {
|
||||
handleUpdate(savedStyle, style);
|
||||
}
|
||||
return savedStyle;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deleteStyleSafe({id, notify = true} = {}) {
|
||||
return onBackgroundReady({id, notify})
|
||||
.then(BG.deleteStyle)
|
||||
.then(() => {
|
||||
if (!notify) {
|
||||
handleDelete(id);
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function download(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.timeout = 10e3;
|
||||
xhr.onloadend = () => (xhr.status == 200
|
||||
? resolve(xhr.responseText)
|
||||
: reject(xhr.status));
|
||||
const [mainUrl, query] = url.split('?');
|
||||
xhr.open(query ? 'POST' : 'GET', mainUrl, true);
|
||||
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
xhr.send(query);
|
||||
});
|
||||
}
|
||||
|
|
137
msgbox/msgbox.css
Normal file
|
@ -0,0 +1,137 @@
|
|||
#message-box {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
box-shadow: 5px 5px 50px rgba(0, 0, 0, 0.35);
|
||||
background-color: rgba(0, 0, 0, .25);
|
||||
animation: fadein .25s ease-in-out;
|
||||
z-index: 9999990;
|
||||
}
|
||||
|
||||
#message-box > div {
|
||||
top: 3rem;
|
||||
right: 3rem;
|
||||
min-width: 10rem;
|
||||
max-width: 50vw;
|
||||
min-height: 5rem;
|
||||
max-height: 90vh;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
box-shadow: 5px 5px 50px rgba(0, 0, 0, 0.35);
|
||||
z-index: 9999991;
|
||||
}
|
||||
|
||||
#message-box.fadeout {
|
||||
animation: fadeout .5s ease-in-out;
|
||||
}
|
||||
|
||||
#message-box.center {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#message-box.center #message-box-contents {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#message-box.center > div {
|
||||
top: unset;
|
||||
right: unset;
|
||||
}
|
||||
|
||||
#message-box-title {
|
||||
font-weight: bold;
|
||||
background-color: rgb(145, 208, 198);
|
||||
padding: .75rem 24px .75rem 52px;
|
||||
font-size: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#message-box-title::before {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
padding: 0 32px 32px 0;
|
||||
background: url(/images/icon/32.png);
|
||||
position: absolute;
|
||||
left: .5rem;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#message-box.danger #message-box-title {
|
||||
background-color: firebrick;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#message-box.danger #message-box-title::before {
|
||||
background: url('/images/icon/32x.png');
|
||||
}
|
||||
|
||||
#message-box.danger #message-box-contents {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#message-box.danger #message-box-close-icon svg:hover {
|
||||
fill: #600;
|
||||
}
|
||||
|
||||
#message-box-close-icon {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
#message-box-close-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
#message-box-contents {
|
||||
overflow: auto;
|
||||
padding: 1.5rem .75rem;
|
||||
position: relative;
|
||||
flex-grow: 9;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#message-box-buttons {
|
||||
padding: .75rem .375rem;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.non-windows #message-box-buttons {
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
#message-box-buttons button {
|
||||
margin: 0 .375rem;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
96
msgbox/msgbox.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
'use strict';
|
||||
|
||||
function messageBox({
|
||||
title, // [mandatory] the title string for innerHTML
|
||||
contents, // [mandatory] 1) DOM element 2) string for innerHTML
|
||||
className = '', // string, CSS class name of the message box element
|
||||
buttons = [], // array of strings used as labels
|
||||
onshow, // function(messageboxElement) invoked after the messagebox is shown
|
||||
blockScroll, // boolean, blocks the page scroll
|
||||
}) { // RETURNS: Promise resolved to {button[number], enter[boolean], esc[boolean]}
|
||||
initOwnListeners();
|
||||
bindGlobalListeners();
|
||||
createElement();
|
||||
document.body.appendChild(messageBox.element);
|
||||
if (onshow) {
|
||||
onshow(messageBox.element);
|
||||
}
|
||||
return new Promise(_resolve => {
|
||||
messageBox.resolve = _resolve;
|
||||
});
|
||||
|
||||
function initOwnListeners() {
|
||||
messageBox.listeners = messageBox.listeners || {
|
||||
closeIcon() {
|
||||
resolveWith({button: -1});
|
||||
},
|
||||
button() {
|
||||
resolveWith({button: this.buttonIndex});
|
||||
},
|
||||
key(event) {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
|
||||
&& (keyCode == 13 || keyCode == 27)) {
|
||||
event.preventDefault();
|
||||
resolveWith(keyCode == 13 ? {enter: true} : {esc: true});
|
||||
}
|
||||
},
|
||||
scroll() {
|
||||
scrollTo(blockScroll.x, blockScroll.y);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function resolveWith(value) {
|
||||
setTimeout(messageBox.resolve, 0, value);
|
||||
animateElement(messageBox.element, {className: 'fadeout', remove: true})
|
||||
.then(unbindAndRemoveSelf);
|
||||
}
|
||||
|
||||
function createElement() {
|
||||
if (messageBox.element) {
|
||||
unbindAndRemoveSelf();
|
||||
}
|
||||
const id = 'message-box';
|
||||
const putAs = typeof contents == 'string' ? 'innerHTML' : 'appendChild';
|
||||
messageBox.element = $element({id, className, appendChild: [
|
||||
$element({appendChild: [
|
||||
$element({id: `${id}-title`, innerHTML: title}),
|
||||
$element({id: `${id}-close-icon`, appendChild:
|
||||
$element({tag: 'SVG#svg', class: 'svg-icon', viewBox: '0 0 20 20', appendChild:
|
||||
$element({tag: 'SVG#path', d: 'M11.69,10l4.55,4.55-1.69,1.69L10,11.69,' +
|
||||
'5.45,16.23,3.77,14.55,8.31,10,3.77,5.45,5.45,3.77,10,8.31l4.55-4.55,1.69,1.69Z',
|
||||
})
|
||||
}),
|
||||
onclick: messageBox.listeners.closeIcon}),
|
||||
$element({id: `${id}-contents`, [putAs]: contents}),
|
||||
$element({id: `${id}-buttons`, appendChild:
|
||||
buttons.map((textContent, buttonIndex) => textContent &&
|
||||
$element({
|
||||
tag: 'button',
|
||||
buttonIndex,
|
||||
textContent,
|
||||
onclick: messageBox.listeners.button,
|
||||
})
|
||||
)
|
||||
}),
|
||||
]}),
|
||||
]});
|
||||
}
|
||||
|
||||
function bindGlobalListeners() {
|
||||
blockScroll = blockScroll && {x: scrollX, y: scrollY};
|
||||
if (blockScroll) {
|
||||
window.addEventListener('scroll', messageBox.listeners.scroll);
|
||||
}
|
||||
window.addEventListener('keydown', messageBox.listeners.key);
|
||||
}
|
||||
|
||||
function unbindAndRemoveSelf() {
|
||||
document.removeEventListener('keydown', messageBox.listeners.key);
|
||||
window.removeEventListener('scroll', messageBox.listeners.scroll);
|
||||
messageBox.element.remove();
|
||||
messageBox.element = null;
|
||||
messageBox.resolve = null;
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/* globals configureCommands */
|
||||
'use strict';
|
||||
|
||||
document.querySelector('#manage-options-button').addEventListener("click", function() {
|
||||
if (chrome.runtime.openOptionsPage) {
|
||||
// Supported (Chrome 42+)
|
||||
chrome.runtime.openOptionsPage();
|
||||
} else {
|
||||
// Fallback
|
||||
window.open(chrome.runtime.getURL('options/index.html'));
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#manage-shortcuts-button').addEventListener("click", configureCommands.open);
|
||||
|
||||
document.querySelector('#editor-styles-button').addEventListener("click", function() {
|
||||
chrome.tabs.create({
|
||||
'url': 'https://userstyles.org/styles/browse/chrome-extension'
|
||||
});
|
||||
});
|
|
@ -1,22 +1,254 @@
|
|||
body {
|
||||
margin: 10px;
|
||||
font-family: "Helvetica Neue",Helvetica,sans-serif;
|
||||
font-size: 12px;
|
||||
html.opera {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
html.opera body {
|
||||
display: inline-block;
|
||||
text-align: initial;
|
||||
}
|
||||
td:last-child {
|
||||
|
||||
html.firefox .block {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
html.firefox #notes {
|
||||
padding-left: calc(6px + 2ex);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-size: 12px;
|
||||
width: calc(16px + 100px + 8px + 260px + 8px + 60px + 4px + 16px);
|
||||
}
|
||||
|
||||
.firefox .chromium-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1em 0;
|
||||
border-bottom: 1px dotted #ccc;
|
||||
padding: 0 0 1em 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.block:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
padding-right: 8px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
margin: .25ex 0;
|
||||
}
|
||||
|
||||
label > * {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
label > :first-child {
|
||||
width: 260px;
|
||||
white-space: normal;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
label:not([disabled]) > :first-child {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
label:not([disabled]):hover > :first-child {
|
||||
text-shadow: 0 0 0.01px rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
button,
|
||||
input[type=number],
|
||||
input[type="color"],
|
||||
.onoffswitch {
|
||||
width: 60px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
|
||||
button {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type=number] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
input[type=number],
|
||||
button {
|
||||
width: 80px;
|
||||
input[type=number]:invalid {
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.notes {
|
||||
input[type="color"] {
|
||||
box-sizing: border-box;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
#actions {
|
||||
justify-content: space-around;
|
||||
align-items: stretch;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
#actions button {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
[data-cmd="check-updates"] button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.update-in-progress [data-cmd="check-updates"] {
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.update-in-progress #update-progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
background-color: currentColor;
|
||||
content: "";
|
||||
opacity: .35;
|
||||
}
|
||||
|
||||
#updates-installed {
|
||||
position: absolute;
|
||||
font-size: 85%;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
#updates-installed:after {
|
||||
content: attr(data-value);
|
||||
margin-left: .5ex;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#updates-installed:not([data-value]),
|
||||
#updates-installed[data-value=""] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#notes {
|
||||
background-color: #f4f4f4;
|
||||
padding: 1.5ex 16px 1ex calc(16px + 2ex);
|
||||
font-size: 90%;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#notes ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#notes li:not(last-child) {
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
#notes a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
#notes a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
#notes p {
|
||||
line-height: 1.25;
|
||||
margin-top: 1ex;
|
||||
margin-bottom: 1ex;
|
||||
}
|
||||
|
||||
sup {
|
||||
vertical-align: baseline;
|
||||
position: relative;
|
||||
top: -0.4em;
|
||||
}
|
||||
|
||||
@keyframes fadeinout {
|
||||
0% { opacity: 0 }
|
||||
10% { opacity: 1 }
|
||||
25% { opacity: 1 }
|
||||
100% { opacity: 0 }
|
||||
}
|
||||
|
||||
/* On/Off FlipSwitch https://proto.io/freebies/onoff/ */
|
||||
|
||||
.onoffswitch {
|
||||
position: relative;
|
||||
margin: 1ex 0;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.onoffswitch input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onoffswitch span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
height: 12px;
|
||||
padding: 0;
|
||||
line-height: 12px;
|
||||
border: 0 solid #E3E3E3;
|
||||
border-radius: 12px;
|
||||
background-color: #E0E0E0;
|
||||
box-shadow: inset 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.onoffswitch span:before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 18px;
|
||||
margin: -3px;
|
||||
background: #efefef;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 46px;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 3px 13px 0 rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.onoffswitch input:checked + span {
|
||||
background-color: #CAEBE3;
|
||||
}
|
||||
|
||||
.onoffswitch input:checked + span, .onoffswitch input:checked + span:before {
|
||||
border-color: #CAEBE3;
|
||||
}
|
||||
|
||||
.onoffswitch input:checked + span .onoffswitch-inner {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.onoffswitch input:checked + span:before {
|
||||
right: 0;
|
||||
background-color: #04BA9F;
|
||||
box-shadow: 3px 6px 18px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
|
|
@ -1,62 +1,109 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html id="stylus">
|
||||
<head>
|
||||
<title>Stylus Options</title>
|
||||
<title i18n-text-append="optionsHeading">Stylus </title>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<script src="../localization.js"></script>
|
||||
<script src="/dom.js"></script>
|
||||
<script src="/messaging.js"></script>
|
||||
<script src="/localization.js"></script>
|
||||
<script src="/prefs.js"></script>
|
||||
<script src="/apply.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1 i18n-text="optionsCustomize"></h1>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n-text="optionsBadgeNormal"></td>
|
||||
<td><input type="color" id="badgeNormal"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n-text="optionsBadgeDisabled"></td>
|
||||
<td><input type="color" id="badgeDisabled"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n-text="optionsPopupWidth"></td>
|
||||
<td><input type="number" id="popupWidth" min="200"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n-text="optionsUpdateInterval"><sup>1</sup></td>
|
||||
<td><input type="number" min="0" id="updateInterval"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button id="save">Save</button>
|
||||
<span id="status"></span>
|
||||
<div id="options">
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizeBadge"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<span i18n-text="prefShowBadge"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="show-badge">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="optionsBadgeNormal"></span>
|
||||
<input type="color" id="badgeNormal">
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="optionsBadgeDisabled"></span>
|
||||
<input type="color" id="badgeDisabled">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizePopup"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<span i18n-text="optionsPopupWidth"></span>
|
||||
<input type="number" id="popupWidth" min="200" max="800">
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="popupStylesFirst"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="popup.stylesFirst">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsCustomizeUpdate"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<span i18n-text="optionsUpdateInterval"><sup>1</sup></span>
|
||||
<input type="number" min="0" id="updateInterval">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<h1 i18n-text="optionsAdvanced"></h1>
|
||||
<div class="items">
|
||||
<label>
|
||||
<span i18n-text="optionsAdvancedExposeIframes"> <sup>2</sup></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="exposeIframes">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<span i18n-text="optionsAdvancedContextDelete"></span>
|
||||
<span class="onoffswitch">
|
||||
<input type="checkbox" id="editor.contextDelete">
|
||||
<span></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block" id="actions">
|
||||
<button data-cmd="reset" i18n-text="optionsResetButton" i18n-title="optionsReset"></button>
|
||||
<button data-cmd="open-manage" i18n-text="optionsOpenManager"></button>
|
||||
<div data-cmd="check-updates">
|
||||
<button i18n-text="optionsCheck" i18n-title="optionsCheckUpdate">
|
||||
<span id="update-progress"></span>
|
||||
</button>
|
||||
<div id="updates-installed" i18n-text="updatesCurrentlyInstalled"></div>
|
||||
</div>
|
||||
<button data-cmd="open-keyboard" class="chromium-only" i18n-text="shortcuts" i18n-title="shortcutsNote"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 i18n-text="optionsActions"></h1>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td i18n-text="optionsOpenManager"><sup>2</sup></td>
|
||||
<td><button type="button" data-cmd="open-manage" i18n-text="optionsOpen"></button></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n-text="optionsCheckUpdate"></td>
|
||||
<td>
|
||||
<span id="update-counter"></span>
|
||||
<button type="button" data-cmd="check-updates" i18n-text="optionsCheck"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="notes">
|
||||
<hr>
|
||||
1: <span i18n-text="optionsUpdateIntervalNote"></span>
|
||||
<br>
|
||||
2: <span i18n-text="optionsOpenManagerNote"></span>, <a href="#" data-cmd="open-keyboard">chrome://extensions/configureCommands</a>
|
||||
<div id="notes">
|
||||
<ol>
|
||||
<li>
|
||||
<p i18n-text="optionsUpdateIntervalNote"></p>
|
||||
<p i18n-text="optionsUpdateImportNote"></p>
|
||||
</li>
|
||||
<li i18n-text="optionsAdvancedExposeIframesNote"></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<script src="/messaging.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
144
options/index.js
|
@ -1,97 +1,63 @@
|
|||
/* globals configureCommands */
|
||||
'use strict';
|
||||
|
||||
function restore () {
|
||||
chrome.runtime.getBackgroundPage(bg => {
|
||||
document.getElementById('badgeDisabled').value = bg.prefs.get('badgeDisabled');
|
||||
document.getElementById('badgeNormal').value = bg.prefs.get('badgeNormal');
|
||||
document.getElementById('popupWidth').value = localStorage.getItem('popupWidth') || '246';
|
||||
document.getElementById('updateInterval').value = bg.prefs.get('updateInterval');
|
||||
});
|
||||
}
|
||||
|
||||
function save () {
|
||||
chrome.runtime.getBackgroundPage(bg => {
|
||||
bg.prefs.set('badgeDisabled', document.getElementById('badgeDisabled').value);
|
||||
bg.prefs.set('badgeNormal', document.getElementById('badgeNormal').value);
|
||||
localStorage.setItem('popupWidth', document.getElementById('popupWidth').value);
|
||||
bg.prefs.set(
|
||||
'updateInterval',
|
||||
Math.max(0, +document.getElementById('updateInterval').value)
|
||||
);
|
||||
// display notification
|
||||
let status = document.getElementById('status');
|
||||
status.textContent = 'Options saved.';
|
||||
setTimeout(() => status.textContent = '', 750);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', restore);
|
||||
document.getElementById('save').addEventListener('click', save);
|
||||
setupLivePrefs();
|
||||
enforceInputRange($('#popupWidth'));
|
||||
|
||||
// actions
|
||||
document.addEventListener('click', e => {
|
||||
let cmd = e.target.dataset.cmd;
|
||||
let total = 0, updated = 0;
|
||||
document.onclick = e => {
|
||||
const target = e.target.closest('[data-cmd]');
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
// prevent double-triggering in case a sub-element was clicked
|
||||
e.stopPropagation();
|
||||
|
||||
function update () {
|
||||
document.getElementById('update-counter').textContent = `${updated}/${total}`;
|
||||
}
|
||||
function done (target) {
|
||||
target.disabled = false;
|
||||
window.setTimeout(() => {
|
||||
document.getElementById('update-counter').textContent = '';
|
||||
}, 750);
|
||||
}
|
||||
switch (target.dataset.cmd) {
|
||||
case 'open-manage':
|
||||
openURL({url: '/manage.html'});
|
||||
break;
|
||||
|
||||
if (cmd === 'open-manage') {
|
||||
chrome.tabs.query({
|
||||
url: chrome.runtime.getURL('manage.html')
|
||||
}, tabs => {
|
||||
if (tabs.length) {
|
||||
chrome.tabs.update(tabs[0].id, {
|
||||
active: true,
|
||||
}, () => {
|
||||
chrome.windows.update(tabs[0].windowId, {
|
||||
focused: true
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
chrome.tabs.create({
|
||||
url: chrome.runtime.getURL('manage.html')
|
||||
});
|
||||
}
|
||||
});
|
||||
case 'check-updates':
|
||||
checkUpdates();
|
||||
break;
|
||||
|
||||
case 'open-keyboard':
|
||||
openURL({url: URLS.configureCommands});
|
||||
e.preventDefault();
|
||||
break;
|
||||
|
||||
case 'reset':
|
||||
$$('input')
|
||||
.filter(input => input.id in prefs.readOnlyValues)
|
||||
.forEach(input => prefs.reset(input.id));
|
||||
break;
|
||||
}
|
||||
else if (cmd === 'check-updates') {
|
||||
e.target.disabled = true;
|
||||
chrome.runtime.getBackgroundPage(bg => {
|
||||
bg.update.perform((cmd, value) => {
|
||||
if (cmd === 'count') {
|
||||
total = value;
|
||||
if (!total) {
|
||||
done(e.target);
|
||||
}
|
||||
}
|
||||
else if (cmd === 'single-updated' || cmd === 'single-skipped') {
|
||||
updated += 1;
|
||||
if (total && updated === total) {
|
||||
done(e.target);
|
||||
}
|
||||
}
|
||||
update();
|
||||
});
|
||||
});
|
||||
// notify the automatic updater to reset the next automatic update accordingly
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'resetInterval'
|
||||
});
|
||||
};
|
||||
|
||||
function checkUpdates() {
|
||||
let total = 0;
|
||||
let checked = 0;
|
||||
let updated = 0;
|
||||
const maxWidth = $('#update-progress').parentElement.clientWidth;
|
||||
BG.updater.checkAllStyles({observer});
|
||||
|
||||
function observer(state, value) {
|
||||
switch (state) {
|
||||
case BG.updater.COUNT:
|
||||
total = value;
|
||||
document.body.classList.add('update-in-progress');
|
||||
break;
|
||||
case BG.updater.UPDATED:
|
||||
updated++;
|
||||
// fallthrough
|
||||
case BG.updater.SKIPPED:
|
||||
checked++;
|
||||
break;
|
||||
case BG.updater.DONE:
|
||||
document.body.classList.remove('update-in-progress');
|
||||
return;
|
||||
}
|
||||
$('#update-progress').style.width = Math.round(checked / total * maxWidth) + 'px';
|
||||
$('#updates-installed').dataset.value = updated || '';
|
||||
}
|
||||
else if (cmd === 'open-keyboard') {
|
||||
configureCommands.open();
|
||||
}
|
||||
});
|
||||
// overwrite the default URL if browser is Opera
|
||||
document.querySelector('[data-cmd="open-keyboard"]').textContent =
|
||||
configureCommands.url;
|
||||
}
|
||||
|
|
404
popup.css
|
@ -1,30 +1,53 @@
|
|||
body {
|
||||
width: 252px;
|
||||
font-size: 12px;
|
||||
font-family: Arial,"Helvetica Neue",Helvetica,sans-serif;
|
||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body > div:not(#installed) {
|
||||
margin-left: 0.75em;
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
|
||||
.firefox .chromium-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#disable-all-wrapper {
|
||||
padding: 0.3em 0 0.6em;
|
||||
}
|
||||
|
||||
#no-styles {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#popup-shortcuts-button {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.checker {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.style-name {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
}
|
||||
a, a:visited {
|
||||
color: black;
|
||||
|
||||
a {
|
||||
color: #000;
|
||||
transition: color .5s;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.left-gutter {
|
||||
|
@ -32,137 +55,289 @@ a, a:visited {
|
|||
width: 16px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.left-gutter input {
|
||||
margin-bottom: 1px;
|
||||
margin-top: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.entry {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
.entry:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
#unavailable,
|
||||
#installed {
|
||||
border-bottom: 1px solid black;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
body > DIV:last-of-type,
|
||||
body.blocked > DIV {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#installed {
|
||||
padding-top: 2px;
|
||||
max-height: 434px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#installed.disabled .style-name {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
#installed .actions {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#installed .actions a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
#installed .style-edit-link {
|
||||
|
||||
/* entry */
|
||||
|
||||
.entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0.75em;
|
||||
}
|
||||
|
||||
.entry:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.entry .style-edit-link {
|
||||
margin-right: 2px;
|
||||
}
|
||||
#installed .style-edit-link, #installed .delete {
|
||||
|
||||
.entry .style-edit-link,
|
||||
.entry .delete {
|
||||
display: inline-block;
|
||||
padding: 0 1px 0;
|
||||
}
|
||||
|
||||
.entry .main-controls {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: calc(100% - 20px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entry .main-controls label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.not-applied .checker,
|
||||
.not-applied .style-name,
|
||||
.not-applied .actions > * {
|
||||
opacity: .2;
|
||||
transition: opacity .5s ease-in-out .25s, color .5s ease-in-out .25s;
|
||||
}
|
||||
|
||||
.not-applied:hover .checker,
|
||||
.not-applied:hover .style-name,
|
||||
.not-applied:hover .actions > * {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.not-applied:hover .style-name {
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.regexp-problem-indicator {
|
||||
background-color: #d00;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
line-height: 15px;
|
||||
border-radius: 8px;
|
||||
margin-right: 6px;
|
||||
margin-left: 6px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.regexp-partial .actions,
|
||||
.regexp-invalid .actions {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
#regexp-explanation {
|
||||
position: fixed;
|
||||
background-color: white;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: .5rem;
|
||||
font-size: 90%;
|
||||
border-top: 2px solid black;
|
||||
border-bottom: 2px solid black;
|
||||
box-shadow: 0 0 100px black;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
#regexp-explanation > div {
|
||||
display: none;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.regexp-partial #regexp-partial,
|
||||
.regexp-invalid #regexp-invalid {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#regexp-explanation > div:not(:last-child) {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
pointer-events: none;
|
||||
transition: fill .5s;
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
fill: #666;
|
||||
}
|
||||
|
||||
a:hover .svg-icon {
|
||||
fill: #000000;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
body > .actions {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.actions > div:not(:last-child):not(#disable-all-wrapper), .actions > .main-controls > div:not(:last-child), #unavailable:not(:last-child), #unavailable + .actions {
|
||||
.actions > div:not(:last-child):not(#disable-all-wrapper),
|
||||
.actions > .main-controls > div:not(:last-child) {
|
||||
margin-bottom: 0.75em;
|
||||
}
|
||||
.actions input, .actions label {
|
||||
|
||||
.actions input,
|
||||
.actions label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#unavailable {
|
||||
border: none;
|
||||
display: none; /* flex */
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
body.blocked #installed,
|
||||
body.blocked #find-styles,
|
||||
body.blocked #write-style,
|
||||
body:not(.blocked) #unavailable {
|
||||
body.blocked #installed > *,
|
||||
body.blocked .actions > .main-controls,
|
||||
body.blocked .actions > .left-gutter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Never shown, but can be enabled with a style */
|
||||
.enable, .disable {
|
||||
|
||||
.enable,
|
||||
.disable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 'New style' links */
|
||||
#write-style-for {margin-right: .6ex}
|
||||
.write-style-link {margin-left: .6ex}
|
||||
.write-style-link::before, .write-style-link::after {font-size: x-small}
|
||||
.write-style-link::before {content: "\00ad"} /* "soft" hyphen */
|
||||
#match {overflow-wrap: break-word;}
|
||||
|
||||
#write-style {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#write-style-for {
|
||||
margin-right: .6ex
|
||||
}
|
||||
|
||||
.write-style-link {
|
||||
margin-left: .6ex
|
||||
}
|
||||
|
||||
.write-style-link::before,
|
||||
.write-style-link::after {
|
||||
font-size: 12px
|
||||
}
|
||||
|
||||
.write-style-link::before {
|
||||
content: "\00ad"; /* "soft" hyphen */
|
||||
}
|
||||
|
||||
#match {
|
||||
overflow-wrap: break-word;
|
||||
display: block;
|
||||
flex-grow: 9;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* "breadcrumbs" 'new style' links */
|
||||
.breadcrumbs > .write-style-link {margin-left: 0}
|
||||
.breadcrumbs:hover a {color: #bbb; text-decoration: none}
|
||||
.breadcrumbs > .write-style-link {
|
||||
margin-left: 0
|
||||
}
|
||||
|
||||
.breadcrumbs:hover a {
|
||||
color: #bbb;
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
/* use just the subdomain name instead of the full domain name */
|
||||
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {
|
||||
font-size: 0
|
||||
}
|
||||
|
||||
/* use just the subdomain name instead of the full domain name */
|
||||
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2)) {font-size: 0}
|
||||
.breadcrumbs > .write-style-link[subdomain]:not(:nth-last-child(2))::before {
|
||||
content: attr(subdomain);
|
||||
}
|
||||
|
||||
/* "dot" after each subdomain name */
|
||||
.breadcrumbs > .write-style-link[subdomain]::after {content: "."}
|
||||
/* no "dot" after top-level domain */
|
||||
.breadcrumbs > .write-style-link:nth-last-child(2)::after {content: none}
|
||||
/* "forward slash" before path ("this URL") */
|
||||
.breadcrumbs > .write-style-link:last-child::before {content: "\200b/"}
|
||||
/* "dot" after each subdomain name */
|
||||
.breadcrumbs > .write-style-link[subdomain]::after {
|
||||
content: "."
|
||||
}
|
||||
|
||||
/* no "dot" after top-level domain */
|
||||
.breadcrumbs > .write-style-link:nth-last-child(2)::after {
|
||||
content: none
|
||||
}
|
||||
|
||||
/* "forward slash" before path ("this URL") */
|
||||
.breadcrumbs > .write-style-link:last-child::before {
|
||||
content: "\200b/"
|
||||
}
|
||||
|
||||
.breadcrumbs > .write-style-link:last-child:first-child::before,
|
||||
.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {content: none}
|
||||
.breadcrumbs > .write-style-link[subdomain=""] + .write-style-link::before {
|
||||
content: none
|
||||
}
|
||||
|
||||
/* suppress TLD-only link */
|
||||
.breadcrumbs > .write-style-link[subdomain=""] {display: none}
|
||||
/* suppress TLD-only link */
|
||||
.breadcrumbs > .write-style-link[subdomain=""] {
|
||||
display: none
|
||||
}
|
||||
|
||||
/* :hover style */
|
||||
.breadcrumbs.url\(\) > .write-style-link, /* :hover or :focus on "this URL" sets class="url()" */
|
||||
/* :hover style */
|
||||
.breadcrumbs.url\(\) > .write-style-link,
|
||||
|
||||
/* :hover or :focus on "this URL" sets class="url()" */
|
||||
.breadcrumbs > .write-style-link:hover,
|
||||
.breadcrumbs > .write-style-link:focus,
|
||||
.breadcrumbs > .write-style-link:hover ~ .write-style-link[subdomain],
|
||||
.breadcrumbs > .write-style-link:focus ~ .write-style-link[subdomain] {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
}
|
||||
|
||||
/* action buttons */
|
||||
/* action buttons */
|
||||
|
||||
#popup-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
padding: 1.2em 0;
|
||||
}
|
||||
|
||||
#popup-options button {
|
||||
margin: 0 2px;
|
||||
width: 33%;
|
||||
|
@ -171,83 +346,126 @@ body:not(.blocked) #unavailable {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* margins */
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
body>div:not(#installed) {
|
||||
margin-left:0.75em;
|
||||
margin-right:0.75em;
|
||||
}
|
||||
#unavailable {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
#installed .entry {
|
||||
}
|
||||
/* entries */
|
||||
#installed .entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 0.75em;
|
||||
}
|
||||
#installed .entry:nth-child(even) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
#installed .main-controls {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: calc(100% - 20px);
|
||||
align-items: center;
|
||||
}
|
||||
#installed .main-controls label {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 5px;
|
||||
}
|
||||
/* confirm */
|
||||
/* confirm */
|
||||
|
||||
#confirm,
|
||||
#confirm>div>span {
|
||||
#confirm > div > span {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#confirm {
|
||||
z-index: 2147483647;
|
||||
display: none; /* flex */
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0!important;
|
||||
margin: 0 !important;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: lights-off .5s cubic-bezier(.03, .67, .08, .94);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
#confirm.lights-on {
|
||||
animation: lights-on .25s ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
#confirm.lights-on > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#confirm[data-display=true] {
|
||||
display: flex;
|
||||
}
|
||||
#confirm>div {
|
||||
|
||||
#confirm > div {
|
||||
width: 80%;
|
||||
height: 100px;
|
||||
max-height: 80%;
|
||||
min-height: 8em;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: solid 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
#confirm>div>span {
|
||||
|
||||
#confirm > div > span {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
}
|
||||
#confirm>div>b {
|
||||
|
||||
#confirm > div > b {
|
||||
padding: 10px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
#confirm>div>div {
|
||||
|
||||
#confirm > div > div {
|
||||
padding: 10px;
|
||||
direction: rtl;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.non-windows #confirm > div > div {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unreachable .entry {
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
.blocked:before,
|
||||
.unreachable:before {
|
||||
padding: 5px 0.75em;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.blocked #installed:before,
|
||||
.unreachable #installed:before {
|
||||
padding: 1px 0.75em 9px;
|
||||
display: block;
|
||||
font-size: 90%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.blocked:before {
|
||||
content: "__MSG_stylusUnavailableForURL__";
|
||||
}
|
||||
|
||||
.blocked #installed:before {
|
||||
content: "__MSG_stylusUnavailableForURLdetails__";
|
||||
}
|
||||
|
||||
.unreachable:before {
|
||||
content: "__MSG_unreachableContentScript__";
|
||||
}
|
||||
|
||||
.unreachable #installed:before {
|
||||
content: "__MSG_unreachableFileHint__";
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
@keyframes lights-off {
|
||||
from {
|
||||
background-color: transparent;
|
||||
}
|
||||
to {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes lights-on {
|
||||
from {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
164
popup.html
|
@ -1,85 +1,109 @@
|
|||
<html>
|
||||
<html id="stylus">
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
|
||||
<template data-id="style">
|
||||
<div>
|
||||
<div class="left-gutter">
|
||||
<input class="checker" type="checkbox">
|
||||
</div>
|
||||
<div class="main-controls">
|
||||
<label class="style-name"></label>
|
||||
<div class="actions">
|
||||
<a href="#" class="enable" i18n-text="enableStyleLabel"></a>
|
||||
<a href="#" class="disable" i18n-text="disableStyleLabel"></a>
|
||||
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel"> <!--`i18n-title` automatically creates `title` attribute -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon edit" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16">
|
||||
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="delete" i18n-title="deleteStyleLabel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-icon remove" fill="hsl(0, 0%, 40%)" height="16" width="14" viewBox="0 0 14 16">
|
||||
<path fill-rule="evenodd" d="M11 2H9c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1H2c-.55 0-1 .45-1 1v1c0 .55.45 1 1 1v9c0 .55.45 1 1 1h7c.55 0 1-.45 1-1V5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 12H3V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9zm1-10H2V3h9v1z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Notes:
|
||||
* Chrome doesn't garbage-collect (or even leaks) SVG <symbol> referenced via <use> so we'll embed the code directly
|
||||
* inter-tag whitespace in templates is automatically removed in localization.js
|
||||
* i18n-anything attribute automatically creates "anything" attribute
|
||||
-->
|
||||
|
||||
<template data-id="style">
|
||||
<div class="entry">
|
||||
<div class="left-gutter">
|
||||
<input class="checker" type="checkbox">
|
||||
</div>
|
||||
<div class="main-controls">
|
||||
<label class="style-name"></label>
|
||||
<div class="actions">
|
||||
<a href="#" class="enable" i18n-text="enableStyleLabel"></a>
|
||||
<a href="#" class="disable" i18n-text="disableStyleLabel"></a>
|
||||
<a class="style-edit-link" href="edit.html?id=" i18n-title="editStyleLabel">
|
||||
<svg class="svg-icon edit" viewBox="0 0 14 16">
|
||||
<path fill-rule="evenodd" d="M0 12v3h3l8-8-3-3-8 8zm3 2H1v-2h1v1h1v1zm10.3-9.3L12 6 9 3l1.3-1.3a.996.996 0 0 1 1.41 0l1.59 1.59c.39.39.39 1.02 0 1.41z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="#" class="delete" i18n-title="deleteStyleLabel">
|
||||
<svg class="svg-icon remove" viewBox="0 0 14 16">
|
||||
<path fill-rule="evenodd" d="M11 2H9c0-.55-.45-1-1-1H5c-.55 0-1 .45-1 1H2c-.55 0-1 .45-1 1v1c0 .55.45 1 1 1v9c0 .55.45 1 1 1h7c.55 0 1-.45 1-1V5c.55 0 1-.45 1-1V3c0-.55-.45-1-1-1zm-1 12H3V5h1v8h1V5h1v8h1V5h1v8h1V5h1v9zm1-10H2V3h9v1z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="localization.js"></script>
|
||||
<script src="health.js"></script>
|
||||
<script src="storage.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<template data-id="writeStyle">
|
||||
<a class="write-style-link"></a>
|
||||
</template>
|
||||
|
||||
<template data-id="noStyles">
|
||||
<div id="no-styles" class="entry" i18n-text="noStylesForSite"></div>
|
||||
</template>
|
||||
|
||||
<template data-id="regexpProblemIndicator">
|
||||
<div class="regexp-problem-indicator" i18n-title="styleRegexpProblemTooltip"></div>
|
||||
</template>
|
||||
|
||||
<template data-id="regexpProblemExplanation">
|
||||
<div id="regexp-explanation">
|
||||
<div id="regexp-partial" i18n-html="styleRegexpPartialExplanation"></div>
|
||||
<div id="regexp-invalid" i18n-text="styleRegexpInvalidExplanation"></div>
|
||||
<button i18n-text="confirmOK"></button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="dom.js"></script>
|
||||
<script src="messaging.js"></script>
|
||||
<script src="localization.js"></script>
|
||||
<script src="prefs.js"></script>
|
||||
<script src="apply.js"></script>
|
||||
<script src="popup.js"></script>
|
||||
</head>
|
||||
|
||||
<body id="stylus-popup">
|
||||
<!-- confirm -->
|
||||
<div id="confirm">
|
||||
<div>
|
||||
<b>Style's Name</b>
|
||||
<span i18n-text="deleteStyleConfirm"></span>
|
||||
<div>
|
||||
<input type="button" i18n-value="confirmCancel" data-cmd="cancel">
|
||||
<input type="button" i18n-value="confirmOK" data-cmd="ok">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="unavailable">
|
||||
<div class="main-controls"><span id="unavailable-message" i18n-text="stylishUnavailableForURL"></span>
|
||||
</div>
|
||||
<div id="confirm">
|
||||
<div>
|
||||
<b>Style's Name</b>
|
||||
<span i18n-text="deleteStyleConfirm"></span>
|
||||
<div>
|
||||
<button i18n-text="confirmDelete" data-cmd="ok"></button>
|
||||
<button i18n-text="confirmCancel" data-cmd="cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="installed"></div>
|
||||
|
||||
<div class="actions">
|
||||
<div id="disable-all-wrapper">
|
||||
<div class="left-gutter">
|
||||
<input id="disableAll" type="checkbox">
|
||||
</div>
|
||||
<div class="main-controls">
|
||||
<label id="disableAll-label" for="disableAll" i18n-text="disableAllStyles"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-gutter"></div>
|
||||
<div class="main-controls">
|
||||
<div id="find-styles">
|
||||
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
|
||||
</div>
|
||||
<div id="write-style">
|
||||
<span id="write-style-for" i18n-text="writeStyleFor"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="installed"></div>
|
||||
<div class="actions">
|
||||
<div id="disable-all-wrapper">
|
||||
<div class="left-gutter">
|
||||
<input id="disableAll" type="checkbox">
|
||||
</div>
|
||||
<div class="main-controls">
|
||||
<label id="disableAll-label" for="disableAll" i18n-text="disableAllStyles"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-gutter"></div>
|
||||
<div class="main-controls">
|
||||
<div id="find-styles">
|
||||
<a id="find-styles-link" href="#" i18n-text="findStylesForSite"></a>
|
||||
</div>
|
||||
<div id="write-style">
|
||||
<span id="write-style-for" i18n-text="writeStyleFor"><br></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div id="popup-options">
|
||||
<button id="popup-manage-button" i18n-text="openManage"></button>
|
||||
<button id="popup-options-button" i18n-text="openOptionsPopup">
|
||||
<button id="popup-shortcuts-button" i18n-text="openShortcutsPopup"></button>
|
||||
<button id="popup-manage-button" i18n-text="openManage" data-href="manage.html"></button>
|
||||
<button id="popup-options-button" i18n-text="openOptionsPopup"></button>
|
||||
<button id="popup-shortcuts-button" class="chromium-only"
|
||||
i18n-text="shortcuts"
|
||||
i18n-title="shortcutsNote"></button>
|
||||
</div>
|
||||
</div>
|
||||
<script src="popup.js"></script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
593
popup.js
|
@ -1,246 +1,407 @@
|
|||
/* globals configureCommands */
|
||||
'use strict';
|
||||
|
||||
var writeStyleTemplate = document.createElement("a");
|
||||
writeStyleTemplate.className = "write-style-link";
|
||||
let installed;
|
||||
let tabURL;
|
||||
const handleEvent = {};
|
||||
|
||||
var installed = document.getElementById("installed");
|
||||
getActiveTabRealURL().then(url => {
|
||||
tabURL = URLS.supported.test(url) ? url : '';
|
||||
Promise.all([
|
||||
tabURL && getStylesSafe({matchUrl: tabURL}),
|
||||
onDOMready().then(() => {
|
||||
initPopup(tabURL);
|
||||
}),
|
||||
]).then(([styles]) => {
|
||||
showStyles(styles);
|
||||
});
|
||||
});
|
||||
|
||||
if (!prefs.get("popup.stylesFirst")) {
|
||||
document.body.insertBefore(document.querySelector("body > .actions"), installed);
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleAdded':
|
||||
case 'styleUpdated':
|
||||
handleUpdate(msg.style);
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.id);
|
||||
break;
|
||||
case 'prefChanged':
|
||||
if ('popup.stylesFirst' in msg.prefs) {
|
||||
const stylesFirst = msg.prefs['popup.stylesFirst'];
|
||||
const actions = $('body > .actions');
|
||||
const before = stylesFirst ? actions : actions.nextSibling;
|
||||
document.body.insertBefore(installed, before);
|
||||
} else if ('popupWidth' in msg.prefs) {
|
||||
setPopupWidth(msg.prefs.popupWidth);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getActiveTabRealURL(updatePopUp);
|
||||
|
||||
function updatePopUp(url) {
|
||||
var urlWillWork = /^(file|http|https|ftps?|chrome\-extension):/.exec(url);
|
||||
if (!urlWillWork) {
|
||||
document.body.classList.add("blocked");
|
||||
document.getElementById("unavailable").style.display = "flex";
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({method: "getStyles", matchUrl: url}, showStyles);
|
||||
document.querySelector("#find-styles a").href = "https://userstyles.org/styles/browse/all/" + encodeURIComponent("file" === urlWillWork[1] ? "file:" : url);
|
||||
|
||||
// Write new style links
|
||||
var writeStyleLinks = [],
|
||||
container = document.createElement('span');
|
||||
container.id = "match";
|
||||
|
||||
// For this URL
|
||||
var urlLink = writeStyleTemplate.cloneNode(true);
|
||||
urlLink.href = "edit.html?url-prefix=" + encodeURIComponent(url);
|
||||
urlLink.appendChild(document.createTextNode( // switchable; default="this URL"
|
||||
!prefs.get("popup.breadcrumbs.usePath")
|
||||
? t("writeStyleForURL").replace(/ /g, "\u00a0")
|
||||
: /\/\/[^/]+\/(.*)/.exec(url)[1]
|
||||
));
|
||||
urlLink.title = "url-prefix(\"$\")".replace("$", url);
|
||||
writeStyleLinks.push(urlLink);
|
||||
document.querySelector("#write-style").appendChild(urlLink)
|
||||
if (prefs.get("popup.breadcrumbs")) { // switchable; default=enabled
|
||||
urlLink.addEventListener("mouseenter", function(event) { this.parentNode.classList.add("url()") }, false);
|
||||
urlLink.addEventListener("focus", function(event) { this.parentNode.classList.add("url()") }, false);
|
||||
urlLink.addEventListener("mouseleave", function(event) { this.parentNode.classList.remove("url()") }, false);
|
||||
urlLink.addEventListener("blur", function(event) { this.parentNode.classList.remove("url()") }, false);
|
||||
}
|
||||
|
||||
// For domain
|
||||
var domains = getDomains(url)
|
||||
domains.forEach(function(domain) {
|
||||
// Don't include TLD
|
||||
if (domains.length > 1 && domain.indexOf(".") == -1) {
|
||||
return;
|
||||
}
|
||||
var domainLink = writeStyleTemplate.cloneNode(true);
|
||||
domainLink.href = "edit.html?domain=" + encodeURIComponent(domain);
|
||||
domainLink.appendChild(document.createTextNode(domain));
|
||||
domainLink.title = "domain(\"$\")".replace("$", domain);
|
||||
domainLink.setAttribute("subdomain", domain.substring(0, domain.indexOf(".")));
|
||||
writeStyleLinks.push(domainLink);
|
||||
});
|
||||
|
||||
var writeStyle = document.querySelector("#write-style");
|
||||
writeStyleLinks.forEach(function(link, index) {
|
||||
link.addEventListener("click", openLinkInTabOrWindow, false);
|
||||
container.appendChild(link);
|
||||
});
|
||||
if (prefs.get("popup.breadcrumbs")) {
|
||||
container.classList.add("breadcrumbs");
|
||||
container.appendChild(container.removeChild(container.firstChild));
|
||||
}
|
||||
writeStyle.appendChild(container);
|
||||
function setPopupWidth(width = prefs.get('popupWidth')) {
|
||||
document.body.style.width =
|
||||
Math.max(200, Math.min(800, width)) + 'px';
|
||||
}
|
||||
|
||||
|
||||
function initPopup(url) {
|
||||
installed = $('#installed');
|
||||
|
||||
setPopupWidth();
|
||||
|
||||
// force Chrome to resize the popup
|
||||
if (!FIREFOX) {
|
||||
document.body.style.height = '10px';
|
||||
document.documentElement.style.height = '10px';
|
||||
}
|
||||
|
||||
// action buttons
|
||||
$('#disableAll').onchange = function() {
|
||||
installed.classList.toggle('disabled', this.checked);
|
||||
};
|
||||
setupLivePrefs();
|
||||
|
||||
$('#find-styles-link').onclick = handleEvent.openURLandHide;
|
||||
$('#popup-manage-button').onclick = handleEvent.openURLandHide;
|
||||
|
||||
$('#popup-options-button').onclick = () => {
|
||||
chrome.runtime.openOptionsPage();
|
||||
window.close();
|
||||
};
|
||||
|
||||
const shortcutsButton = $('#popup-shortcuts-button');
|
||||
shortcutsButton.dataset.href = URLS.configureCommands;
|
||||
shortcutsButton.onclick = handleEvent.openURLandHide;
|
||||
|
||||
if (!prefs.get('popup.stylesFirst')) {
|
||||
document.body.insertBefore(
|
||||
$('body > .actions'),
|
||||
installed);
|
||||
}
|
||||
|
||||
// find styles link
|
||||
$('#find-styles a').href =
|
||||
'https://userstyles.org/styles/browse/all/' +
|
||||
encodeURIComponent(url.startsWith('file:') ? 'file:' : url);
|
||||
|
||||
if (!url) {
|
||||
document.body.classList.add('blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
getActiveTab().then(tab => {
|
||||
chrome.tabs.sendMessage(tab.id, {method: 'ping'}, {frameId: 0}, pong => {
|
||||
if (pong === undefined) {
|
||||
document.body.classList.add('unreachable');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Write new style links
|
||||
const writeStyle = $('#write-style');
|
||||
const matchTargets = document.createElement('span');
|
||||
const matchWrapper = document.createElement('span');
|
||||
matchWrapper.id = 'match';
|
||||
matchWrapper.appendChild(matchTargets);
|
||||
|
||||
// For this URL
|
||||
const urlLink = template.writeStyle.cloneNode(true);
|
||||
Object.assign(urlLink, {
|
||||
href: 'edit.html?url-prefix=' + encodeURIComponent(url),
|
||||
title: `url-prefix("${url}")`,
|
||||
textContent: prefs.get('popup.breadcrumbs.usePath')
|
||||
? new URL(url).pathname.slice(1)
|
||||
: t('writeStyleForURL').replace(/ /g, '\u00a0'), // this URL
|
||||
onclick: handleEvent.openLink,
|
||||
});
|
||||
if (prefs.get('popup.breadcrumbs')) {
|
||||
urlLink.onmouseenter =
|
||||
urlLink.onfocus = () => urlLink.parentNode.classList.add('url()');
|
||||
urlLink.onmouseleave =
|
||||
urlLink.onblur = () => urlLink.parentNode.classList.remove('url()');
|
||||
}
|
||||
matchTargets.appendChild(urlLink);
|
||||
|
||||
// For domain
|
||||
const domains = BG.getDomains(url);
|
||||
for (const domain of domains) {
|
||||
// Don't include TLD
|
||||
if (domains.length > 1 && !domain.includes('.')) {
|
||||
continue;
|
||||
}
|
||||
const domainLink = template.writeStyle.cloneNode(true);
|
||||
Object.assign(domainLink, {
|
||||
href: 'edit.html?domain=' + encodeURIComponent(domain),
|
||||
textContent: domain,
|
||||
title: `domain("${domain}")`,
|
||||
onclick: handleEvent.openLink,
|
||||
});
|
||||
domainLink.setAttribute('subdomain', domain.substring(0, domain.indexOf('.')));
|
||||
matchTargets.appendChild(domainLink);
|
||||
}
|
||||
|
||||
if (prefs.get('popup.breadcrumbs')) {
|
||||
matchTargets.classList.add('breadcrumbs');
|
||||
matchTargets.appendChild(matchTargets.removeChild(matchTargets.firstElementChild));
|
||||
}
|
||||
writeStyle.appendChild(matchWrapper);
|
||||
}
|
||||
|
||||
|
||||
function showStyles(styles) {
|
||||
var enabledFirst = prefs.get("popup.enabledFirst");
|
||||
styles.sort(function(a, b) {
|
||||
if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
if (styles.length == 0) {
|
||||
installed.innerHTML = "<div class='entry' id='no-styles'>" + t('noStylesForSite') + "</div>";
|
||||
}
|
||||
styles.map(createStyleElement).forEach(function(e) {
|
||||
installed.appendChild(e);
|
||||
});
|
||||
// force Chrome to resize the popup
|
||||
document.body.style.height = '10px';
|
||||
document.documentElement.style.height = '10px';
|
||||
if (!styles) {
|
||||
return;
|
||||
}
|
||||
if (!styles.length) {
|
||||
installed.innerHTML = template.noStyles.outerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
const enabledFirst = prefs.get('popup.enabledFirst');
|
||||
styles.sort((a, b) => (
|
||||
enabledFirst && a.enabled !== b.enabled
|
||||
? !(a.enabled < b.enabled) ? -1 : 1
|
||||
: a.name.localeCompare(b.name)
|
||||
));
|
||||
|
||||
let postponeDetect = false;
|
||||
const t0 = performance.now();
|
||||
const container = document.createDocumentFragment();
|
||||
for (const style of styles) {
|
||||
createStyleElement({style, container, postponeDetect});
|
||||
postponeDetect = postponeDetect || performance.now() - t0 > 100;
|
||||
}
|
||||
installed.appendChild(container);
|
||||
|
||||
getStylesSafe({matchUrl: tabURL, strictRegexp: false})
|
||||
.then(unscreenedStyles => {
|
||||
for (const unscreened of unscreenedStyles) {
|
||||
if (!styles.includes(unscreened)) {
|
||||
postponeDetect = postponeDetect || performance.now() - t0 > 100;
|
||||
createStyleElement({
|
||||
style: Object.assign({appliedSections: [], postponeDetect}, unscreened),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createStyleElement(style) {
|
||||
var e = template.style.cloneNode(true);
|
||||
var checkbox = e.querySelector(".checker");
|
||||
checkbox.id = "style-" + style.id;
|
||||
checkbox.checked = style.enabled;
|
||||
|
||||
e.setAttribute("class", "entry " + (style.enabled ? "enabled" : "disabled"));
|
||||
e.setAttribute("style-id", style.id);
|
||||
var styleName = e.querySelector(".style-name");
|
||||
styleName.appendChild(document.createTextNode(style.name));
|
||||
styleName.setAttribute("for", "style-" + style.id);
|
||||
styleName.checkbox = checkbox;
|
||||
var editLink = e.querySelector(".style-edit-link");
|
||||
editLink.setAttribute("href", editLink.getAttribute("href") + style.id);
|
||||
editLink.addEventListener("click", openLinkInTabOrWindow, false);
|
||||
function createStyleElement({
|
||||
style,
|
||||
container = installed,
|
||||
postponeDetect,
|
||||
}) {
|
||||
const entry = template.style.cloneNode(true);
|
||||
entry.setAttribute('style-id', style.id);
|
||||
Object.assign(entry, {
|
||||
id: 'style-' + style.id,
|
||||
styleId: style.id,
|
||||
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
|
||||
onmousedown: handleEvent.maybeEdit,
|
||||
});
|
||||
|
||||
styleName.addEventListener("click", function() { this.checkbox.click(); event.preventDefault(); });
|
||||
// clicking the checkbox will toggle it, and this will run after that happens
|
||||
checkbox.addEventListener("click", function() { enable(event, event.target.checked); }, false);
|
||||
e.querySelector(".enable").addEventListener("click", function() { enable(event, true); }, false);
|
||||
e.querySelector(".disable").addEventListener("click", function() { enable(event, false); }, false);
|
||||
const checkbox = $('.checker', entry);
|
||||
Object.assign(checkbox, {
|
||||
id: 'style-' + style.id,
|
||||
checked: style.enabled,
|
||||
onclick: handleEvent.toggle,
|
||||
});
|
||||
|
||||
e.querySelector(".delete").addEventListener("click", function() { doDelete(event, false); }, false);
|
||||
return e;
|
||||
const editLink = $('.style-edit-link', entry);
|
||||
Object.assign(editLink, {
|
||||
href: editLink.getAttribute('href') + style.id,
|
||||
onclick: handleEvent.openLink,
|
||||
});
|
||||
|
||||
const styleName = $('.style-name', entry);
|
||||
Object.assign(styleName, {
|
||||
htmlFor: 'style-' + style.id,
|
||||
onclick: handleEvent.name,
|
||||
});
|
||||
styleName.checkbox = checkbox;
|
||||
styleName.appendChild(document.createTextNode(style.name));
|
||||
|
||||
$('.enable', entry).onclick = handleEvent.toggle;
|
||||
$('.disable', entry).onclick = handleEvent.toggle;
|
||||
$('.delete', entry).onclick = handleEvent.delete;
|
||||
|
||||
if (postponeDetect) {
|
||||
setTimeout(detectSloppyRegexps, 0, {entry, style});
|
||||
} else {
|
||||
detectSloppyRegexps({entry, style});
|
||||
}
|
||||
|
||||
const oldElement = $('#style-' + style.id);
|
||||
if (oldElement) {
|
||||
oldElement.parentNode.replaceChild(entry, oldElement);
|
||||
} else {
|
||||
container.appendChild(entry);
|
||||
}
|
||||
}
|
||||
|
||||
function enable(event, enabled) {
|
||||
var id = getId(event);
|
||||
enableStyle(id, enabled);
|
||||
}
|
||||
|
||||
function doDelete() {
|
||||
document.getElementById('confirm').dataset.display = true;
|
||||
let id = getId(event);
|
||||
document.querySelector('#confirm b').textContent =
|
||||
document.querySelector(`[style-id="${id}"] label`).textContent;
|
||||
document.getElementById('confirm').dataset.id = id;
|
||||
Object.assign(handleEvent, {
|
||||
|
||||
}
|
||||
document.getElementById('confirm').addEventListener('click', e => {
|
||||
let cmd = e.target.dataset.cmd;
|
||||
if (cmd === 'ok') {
|
||||
deleteStyle(document.getElementById('confirm').dataset.id, () => {
|
||||
// update view with 'No styles installed for this site' message
|
||||
if (document.getElementById('installed').children.length === 0) {
|
||||
showStyles([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
//
|
||||
if (cmd) {
|
||||
document.getElementById('confirm').dataset.display = false;
|
||||
}
|
||||
getClickedStyleId(event) {
|
||||
return (handleEvent.getClickedStyleElement(event) || {}).styleId;
|
||||
},
|
||||
|
||||
getClickedStyleElement(event) {
|
||||
return event.target.closest('.entry');
|
||||
},
|
||||
|
||||
name(event) {
|
||||
this.checkbox.click();
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
toggle(event) {
|
||||
saveStyleSafe({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.type == 'checkbox' ? this.checked : this.matches('.enable'),
|
||||
});
|
||||
},
|
||||
|
||||
delete(event) {
|
||||
const id = handleEvent.getClickedStyleId(event);
|
||||
const box = $('#confirm');
|
||||
box.dataset.display = true;
|
||||
box.style.cssText = '';
|
||||
$('b', box).textContent = (BG.cachedStyles.byId.get(id) || {}).name;
|
||||
$('[data-cmd="ok"]', box).onclick = () => confirm(true);
|
||||
$('[data-cmd="cancel"]', box).onclick = () => confirm(false);
|
||||
window.onkeydown = event => {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey
|
||||
&& (keyCode == 13 || keyCode == 27)) {
|
||||
event.preventDefault();
|
||||
confirm(keyCode == 13);
|
||||
}
|
||||
};
|
||||
function confirm(ok) {
|
||||
window.onkeydown = null;
|
||||
animateElement(box, {className: 'lights-on'})
|
||||
.then(() => (box.dataset.display = false));
|
||||
if (ok) {
|
||||
deleteStyleSafe({id}).then(() => {
|
||||
// update view with 'No styles installed for this site' message
|
||||
if (!installed.children.length) {
|
||||
showStyles([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
indicator(event) {
|
||||
const entry = handleEvent.getClickedStyleElement(event);
|
||||
const info = template.regexpProblemExplanation.cloneNode(true);
|
||||
$$('#' + info.id).forEach(el => el.remove());
|
||||
$$('a', info).forEach(el => (el.onclick = handleEvent.openURLandHide));
|
||||
$$('button', info).forEach(el => (el.onclick = handleEvent.closeExplanation));
|
||||
entry.appendChild(info);
|
||||
},
|
||||
|
||||
closeExplanation() {
|
||||
$('#regexp-explanation').remove();
|
||||
},
|
||||
|
||||
openLink(event) {
|
||||
if (!prefs.get('openEditInWindow', false)) {
|
||||
handleEvent.openURLandHide.call(this, event);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
chrome.windows.create(
|
||||
Object.assign({
|
||||
url: this.href
|
||||
}, prefs.get('windowPosition', {}))
|
||||
);
|
||||
close();
|
||||
},
|
||||
|
||||
maybeEdit(event) {
|
||||
if (!(
|
||||
event.button == 0 && (event.ctrlKey || event.metaKey) ||
|
||||
event.button == 1 ||
|
||||
event.button == 2)) {
|
||||
return;
|
||||
}
|
||||
// open an editor on middleclick
|
||||
if (event.target.matches('.entry, .style-name, .style-edit-link')) {
|
||||
this.onmouseup = () => $('.style-edit-link', this).click();
|
||||
this.oncontextmenu = event => event.preventDefault();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// prevent the popup being opened in a background tab
|
||||
// when an irrelevant link was accidentally clicked
|
||||
if (event.target.closest('a')) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
openURLandHide(event) {
|
||||
event.preventDefault();
|
||||
openURL({url: this.href || this.dataset.href})
|
||||
.then(window.close);
|
||||
},
|
||||
});
|
||||
|
||||
function getBrowser() {
|
||||
if (navigator.userAgent.indexOf("OPR") > -1) {
|
||||
return "Opera";
|
||||
}
|
||||
return "Chrome";
|
||||
}
|
||||
|
||||
function getId(event) {
|
||||
var e = event.target;
|
||||
while (e) {
|
||||
if (e.hasAttribute("style-id")) {
|
||||
return e.getAttribute("style-id");
|
||||
}
|
||||
e = e.parentNode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openLinkInTabOrWindow(event) {
|
||||
event.preventDefault();
|
||||
if (prefs.get("openEditInWindow", false)) {
|
||||
var options = {url: event.target.href}
|
||||
var wp = prefs.get("windowPosition", {});
|
||||
for (var k in wp) options[k] = wp[k];
|
||||
chrome.windows.create(options);
|
||||
} else {
|
||||
openLink(event);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
function openLink(event) {
|
||||
event.preventDefault();
|
||||
chrome.runtime.sendMessage({method: "openURL", url: event.target.href});
|
||||
close();
|
||||
}
|
||||
|
||||
function handleUpdate(style) {
|
||||
var styleElement = installed.querySelector("[style-id='" + style.id + "']");
|
||||
if (styleElement) {
|
||||
installed.replaceChild(createStyleElement(style), styleElement);
|
||||
} else {
|
||||
getActiveTabRealURL(function(url) {
|
||||
if (chrome.extension.getBackgroundPage().getApplicableSections(style, url).length) {
|
||||
// a new style for the current url is installed
|
||||
document.getElementById("unavailable").style.display = "none";
|
||||
installed.appendChild(createStyleElement(style));
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($('#style-' + style.id)) {
|
||||
createStyleElement({style});
|
||||
return;
|
||||
}
|
||||
// Add an entry when a new style for the current url is installed
|
||||
if (tabURL && BG.getApplicableSections({style, matchUrl: tabURL, stopOnFirst: true}).length) {
|
||||
document.body.classList.remove('blocked');
|
||||
createStyleElement({style});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleDelete(id) {
|
||||
var styleElement = installed.querySelector("[style-id='" + id + "']");
|
||||
if (styleElement) {
|
||||
installed.removeChild(styleElement);
|
||||
}
|
||||
$$('#style-' + id).forEach(el => el.remove());
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
|
||||
if (request.method == "updatePopup") {
|
||||
switch (request.reason) {
|
||||
case "styleAdded":
|
||||
case "styleUpdated":
|
||||
handleUpdate(request.style);
|
||||
break;
|
||||
case "styleDeleted":
|
||||
handleDelete(request.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
["find-styles-link"].forEach(function(id) {
|
||||
document.getElementById(id).addEventListener("click", openLink, false);
|
||||
});
|
||||
/*
|
||||
According to CSS4 @document specification the entire URL must match.
|
||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||
We'll detect styles that abuse the bug by finding the sections that
|
||||
would have been applied by Stylish but not by us as we follow the spec.
|
||||
Additionally we'll check for invalid regexps.
|
||||
*/
|
||||
function detectSloppyRegexps({entry, style}) {
|
||||
const {
|
||||
appliedSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL}),
|
||||
wannabeSections =
|
||||
BG.getApplicableSections({style, matchUrl: tabURL, strictRegexp: false}),
|
||||
} = style;
|
||||
|
||||
document.getElementById("disableAll").addEventListener("change", function(event) {
|
||||
installed.classList.toggle("disabled", prefs.get("disableAll"));
|
||||
});
|
||||
setupLivePrefs(["disableAll"]);
|
||||
BG.compileStyleRegExps({style, compileAll: true});
|
||||
entry.hasInvalidRegexps = wannabeSections.some(section =>
|
||||
section.regexps.some(rx => !BG.cachedStyles.regexps.has(rx)));
|
||||
entry.sectionsSkipped = wannabeSections.length - appliedSections.length;
|
||||
|
||||
document.querySelector('#popup-manage-button').addEventListener("click", function() {
|
||||
window.open(chrome.runtime.getURL('manage.html'));
|
||||
});
|
||||
|
||||
document.querySelector('#popup-options-button').addEventListener("click", function() {
|
||||
if (chrome.runtime.openOptionsPage) {
|
||||
// Supported (Chrome 42+)
|
||||
chrome.runtime.openOptionsPage();
|
||||
} else {
|
||||
// Fallback
|
||||
window.open(chrome.runtime.getURL('options/index.html'));
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelector('#popup-shortcuts-button').addEventListener("click", configureCommands.open);
|
||||
|
||||
// popup width
|
||||
document.body.style.width = (localStorage.getItem('popupWidth') || '246') + 'px';
|
||||
if (!appliedSections.length) {
|
||||
entry.classList.add('not-applied');
|
||||
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
|
||||
}
|
||||
if (entry.sectionsSkipped || entry.hasInvalidRegexps) {
|
||||
entry.classList.toggle('regexp-partial', entry.sectionsSkipped);
|
||||
entry.classList.toggle('regexp-invalid', entry.hasInvalidRegexps);
|
||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||
indicator.appendChild(document.createTextNode(entry.sectionsSkipped || '!'));
|
||||
indicator.onclick = handleEvent.indicator;
|
||||
$('.main-controls', entry).appendChild(indicator);
|
||||
}
|
||||
}
|
||||
|
|
352
prefs.js
Normal file
|
@ -0,0 +1,352 @@
|
|||
/* global prefs: true, contextMenus */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var prefs = new function Prefs() {
|
||||
const defaults = {
|
||||
'openEditInWindow': false, // new editor opens in a own browser window
|
||||
'windowPosition': {}, // detached window position
|
||||
'show-badge': true, // display text on popup menu icon
|
||||
'disableAll': false, // boss key
|
||||
'exposeIframes': false, // Add 'stylus-iframe' attribute to HTML element in all iframes
|
||||
|
||||
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
||||
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
||||
'popup.enabledFirst': true, // display enabled styles before disabled styles
|
||||
'popup.stylesFirst': true, // display enabled styles before disabled styles
|
||||
|
||||
'manage.onlyEnabled': false, // display only enabled styles
|
||||
'manage.onlyLocal': false, // display only styles created locally
|
||||
'manage.newUI': true, // use the new compact layout
|
||||
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
|
||||
'manage.newUI.faviconsGray': true, // gray out favicons
|
||||
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
|
||||
|
||||
'editor.options': {}, // CodeMirror.defaults.*
|
||||
'editor.lineWrapping': true, // word wrap
|
||||
'editor.smartIndent': true, // 'smart' indent
|
||||
'editor.indentWithTabs': false, // smart indent with tabs
|
||||
'editor.tabSize': 4, // tab width, in spaces
|
||||
'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
|
||||
'editor.theme': 'default', // CSS theme
|
||||
'editor.beautify': { // CSS beautifier
|
||||
selector_separator_newline: true,
|
||||
newline_before_open_brace: false,
|
||||
newline_after_open_brace: true,
|
||||
newline_between_properties: true,
|
||||
newline_before_close_brace: true,
|
||||
newline_between_rules: false,
|
||||
end_with_newline: false,
|
||||
space_around_selector_separator: true,
|
||||
},
|
||||
'editor.lintDelay': 500, // lint gutter marker update delay, ms
|
||||
'editor.lintReportDelay': 4500, // lint report update delay, ms
|
||||
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
|
||||
// selection = only when something is selected
|
||||
// '' (empty string) = disabled
|
||||
'editor.contextDelete': contextDeleteMissing(), // "Delete" item in context menu
|
||||
|
||||
'badgeDisabled': '#8B0000', // badge background color when disabled
|
||||
'badgeNormal': '#006666', // badge background color
|
||||
|
||||
'popupWidth': 246, // popup width in pixels
|
||||
|
||||
'updateInterval': 24, // user-style automatic update interval, hours (0 = disable)
|
||||
};
|
||||
const values = deepCopy(defaults);
|
||||
|
||||
const affectsIcon = [
|
||||
'show-badge',
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
];
|
||||
|
||||
const onChange = {
|
||||
any: new Set(),
|
||||
specific: new Map(),
|
||||
};
|
||||
|
||||
// coalesce multiple pref changes in broadcast
|
||||
let broadcastPrefs = {};
|
||||
|
||||
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
||||
|
||||
Object.assign(Prefs.prototype, {
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in values) {
|
||||
return values[key];
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
if (key in defaults) {
|
||||
return defaults[key];
|
||||
}
|
||||
console.warn("No default preference for '%s'", key);
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return deepCopy(values);
|
||||
},
|
||||
|
||||
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) {
|
||||
const oldValue = values[key];
|
||||
switch (typeof defaults[key]) {
|
||||
case typeof value:
|
||||
break;
|
||||
case 'string':
|
||||
value = String(value);
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
value = value === true || value === 'true';
|
||||
break;
|
||||
}
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
const hasChanged = !equal(value, oldValue);
|
||||
if (!fromBroadcast) {
|
||||
if (BG && BG != window) {
|
||||
BG.prefs.set(key, BG.deepCopy(value), {broadcast, sync});
|
||||
} else {
|
||||
localStorage[key] = typeof defaults[key] == 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
if (broadcast && hasChanged) {
|
||||
this.broadcast(key, value, {sync});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
const listener = onChange.specific.get(key);
|
||||
if (listener) {
|
||||
listener(key, value);
|
||||
}
|
||||
for (const listener of onChange.any.values()) {
|
||||
listener(key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
remove: key => this.set(key, undefined),
|
||||
|
||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
||||
|
||||
broadcast(key, value, {sync = true} = {}) {
|
||||
broadcastPrefs[key] = value;
|
||||
debounce(doBroadcast);
|
||||
if (sync) {
|
||||
debounce(doSyncSet);
|
||||
}
|
||||
},
|
||||
|
||||
subscribe(listener, keys) {
|
||||
if (keys) {
|
||||
for (const key of keys) {
|
||||
onChange.specific.set(key, listener);
|
||||
}
|
||||
} else {
|
||||
onChange.any.add(listener);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Unlike sync, HTML5 localStorage is ready at browser startup
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults
|
||||
// during the startup phase
|
||||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value == 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
value = defaultValue;
|
||||
}
|
||||
if (BG == window) {
|
||||
// when in bg page, .set() will write to localStorage
|
||||
this.set(key, value, {broadcast: false, sync: false});
|
||||
} else {
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!BG || BG == window) {
|
||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
||||
|
||||
getSync().get('settings', ({settings: synced} = {}) => {
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
|
||||
// this is a fix for the period when popupWidth wasn't synced
|
||||
// TODO: remove it in a couple of months
|
||||
continue;
|
||||
}
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {sync: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area == 'sync' && 'settings' in changes) {
|
||||
const synced = changes.settings.newValue;
|
||||
if (synced) {
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {sync: false});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// user manually deleted our settings, we'll recreate them
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// any access to chrome API takes time due to initialization of bindings
|
||||
window.addEventListener('load', function _() {
|
||||
window.removeEventListener('load', _);
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
if (msg.prefs) {
|
||||
for (const id in msg.prefs) {
|
||||
prefs.set(id, msg.prefs[id], {fromBroadcast: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
function doBroadcast() {
|
||||
const affects = {
|
||||
all: 'disableAll' in broadcastPrefs
|
||||
|| 'exposeIframes' in broadcastPrefs,
|
||||
};
|
||||
if (!affects.all) {
|
||||
for (const key in broadcastPrefs) {
|
||||
affects.icon = affects.icon || affectsIcon.includes(key);
|
||||
affects.popup = affects.popup || key.startsWith('popup');
|
||||
affects.editor = affects.editor || key.startsWith('editor');
|
||||
affects.manager = affects.manager || key.startsWith('manage');
|
||||
}
|
||||
}
|
||||
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
||||
broadcastPrefs = {};
|
||||
}
|
||||
|
||||
function doSyncSet() {
|
||||
getSync().set({'settings': values});
|
||||
}
|
||||
|
||||
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
||||
function getSync() {
|
||||
if ('sync' in chrome.storage) {
|
||||
return chrome.storage.sync;
|
||||
}
|
||||
const crappyStorage = {};
|
||||
return {
|
||||
get(key, callback) {
|
||||
callback(crappyStorage[key] || {});
|
||||
},
|
||||
set(source, callback) {
|
||||
for (const property in source) {
|
||||
if (source.hasOwnProperty(property)) {
|
||||
crappyStorage[property] = source[property];
|
||||
}
|
||||
}
|
||||
callback();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy == 'object') {
|
||||
Object.freeze(copy);
|
||||
}
|
||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
||||
}
|
||||
|
||||
function equal(a, b) {
|
||||
if (!a || !b || typeof a != 'object' || typeof b != 'object') {
|
||||
return a === b;
|
||||
}
|
||||
if (Object.keys(a).length != Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (const k in a) {
|
||||
if (typeof a[k] == 'object') {
|
||||
if (!equal(a[k], b[k])) {
|
||||
return false;
|
||||
}
|
||||
} else if (a[k] !== b[k]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function contextDeleteMissing() {
|
||||
return (
|
||||
// detect browsers without Delete by looking at the end of UA string
|
||||
/Vivaldi\/[\d.]+$/.test(navigator.userAgent) ||
|
||||
// Chrome and co.
|
||||
/Safari\/[\d.]+$/.test(navigator.userAgent) &&
|
||||
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
|
||||
!Array.from(navigator.plugins).some(p => p.name == 'Shockwave Flash')
|
||||
);
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||
// and establishes a two-way connection between the document elements and the actual prefs
|
||||
function setupLivePrefs(
|
||||
IDs = Object.getOwnPropertyNames(prefs.readOnlyValues)
|
||||
.filter(id => document.getElementById(id))
|
||||
) {
|
||||
const checkedProps = {};
|
||||
for (const id of IDs) {
|
||||
const element = document.getElementById(id);
|
||||
checkedProps[id] = element.type == 'checkbox' ? 'checked' : 'value';
|
||||
updateElement({id, element, force: true});
|
||||
element.addEventListener('change', onChange);
|
||||
}
|
||||
prefs.subscribe((id, value) => updateElement({id, value}), IDs);
|
||||
|
||||
function onChange() {
|
||||
const value = this[checkedProps[this.id]];
|
||||
if (prefs.get(this.id) != value) {
|
||||
prefs.set(this.id, value);
|
||||
}
|
||||
}
|
||||
function updateElement({
|
||||
id,
|
||||
value = prefs.get(id),
|
||||
element = document.getElementById(id),
|
||||
force,
|
||||
}) {
|
||||
const prop = checkedProps[id];
|
||||
if (force || element[prop] != value) {
|
||||
element[prop] = value;
|
||||
element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
var webSqlStorage = {
|
||||
|
||||
migrate: function() {
|
||||
if (typeof openDatabase == "undefined") {
|
||||
// No WebSQL - no migration!
|
||||
return;
|
||||
}
|
||||
webSqlStorage.getStyles(function(styles) {
|
||||
getDatabase(function(db) {
|
||||
var tx = db.transaction(["styles"], "readwrite");
|
||||
var os = tx.objectStore("styles");
|
||||
styles.forEach(function(s) {
|
||||
webSqlStorage.cleanStyle(s)
|
||||
os.add(s);
|
||||
});
|
||||
// While this was running, the styles were loaded from the (empty) indexed db
|
||||
setTimeout(function() {
|
||||
invalidateCache(true);
|
||||
}, 500);
|
||||
});
|
||||
}, null);
|
||||
},
|
||||
|
||||
cleanStyle: function(s) {
|
||||
delete s.id;
|
||||
s.sections.forEach(function(section) {
|
||||
delete section.id;
|
||||
["urls", "urlPrefixes", "domains", "regexps"].forEach(function(property) {
|
||||
if (!section[property]) {
|
||||
section[property] = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
getStyles: function(callback) {
|
||||
webSqlStorage.getDatabase(function(db) {
|
||||
if (!db) {
|
||||
callback([]);
|
||||
return;
|
||||
}
|
||||
db.readTransaction(function (t) {
|
||||
var where = "";
|
||||
var params = [];
|
||||
|
||||
t.executeSql('SELECT DISTINCT s.*, se.id section_id, se.code, sm.name metaName, sm.value metaValue FROM styles s LEFT JOIN sections se ON se.style_id = s.id LEFT JOIN section_meta sm ON sm.section_id = se.id WHERE 1' + where + ' ORDER BY s.id, se.id, sm.id', params, function (t, r) {
|
||||
var styles = [];
|
||||
var currentStyle = null;
|
||||
var currentSection = null;
|
||||
for (var i = 0; i < r.rows.length; i++) {
|
||||
var values = r.rows.item(i);
|
||||
var metaName = null;
|
||||
switch (values.metaName) {
|
||||
case null:
|
||||
break;
|
||||
case "url":
|
||||
metaName = "urls";
|
||||
break;
|
||||
case "url-prefix":
|
||||
metaName = "urlPrefixes";
|
||||
break;
|
||||
case "domain":
|
||||
var metaName = "domains";
|
||||
break;
|
||||
case "regexps":
|
||||
var metaName = "regexps";
|
||||
break;
|
||||
default:
|
||||
var metaName = values.metaName + "s";
|
||||
}
|
||||
var metaValue = values.metaValue;
|
||||
if (currentStyle == null || currentStyle.id != values.id) {
|
||||
currentStyle = {id: values.id, url: values.url, updateUrl: values.updateUrl, md5Url: values.md5Url, name: values.name, enabled: values.enabled == "true", originalMd5: values.originalMd5, sections: []};
|
||||
styles.push(currentStyle);
|
||||
}
|
||||
if (values.section_id != null) {
|
||||
if (currentSection == null || currentSection.id != values.section_id) {
|
||||
currentSection = {id: values.section_id, code: values.code};
|
||||
currentStyle.sections.push(currentSection);
|
||||
}
|
||||
if (metaName && metaValue) {
|
||||
if (currentSection[metaName]) {
|
||||
currentSection[metaName].push(metaValue);
|
||||
} else {
|
||||
currentSection[metaName] = [metaValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
callback(styles);
|
||||
}, reportError);
|
||||
}, reportError);
|
||||
}, reportError);
|
||||
},
|
||||
|
||||
getDatabase: function(ready, error) {
|
||||
try {
|
||||
stylishDb = openDatabase('stylish', '', 'Stylish Styles', 5*1024*1024);
|
||||
} catch (ex) {
|
||||
error();
|
||||
throw ex;
|
||||
}
|
||||
if (stylishDb.version == "") {
|
||||
// It didn't already exist, we have nothing to migrate.
|
||||
ready(null);
|
||||
return;
|
||||
}
|
||||
if (stylishDb.version == "1.0") {
|
||||
webSqlStorage.dbV11(stylishDb, error, ready);
|
||||
} else if (stylishDb.version == "1.1") {
|
||||
webSqlStorage.dbV12(stylishDb, error, ready);
|
||||
} else if (stylishDb.version == "1.2") {
|
||||
webSqlStorage.dbV13(stylishDb, error, ready);
|
||||
} else if (stylishDb.version == "1.3") {
|
||||
webSqlStorage.dbV14(stylishDb, error, ready);
|
||||
} else if (stylishDb.version == "1.4") {
|
||||
webSqlStorage.dbV15(stylishDb, error, ready);
|
||||
} else {
|
||||
ready(stylishDb);
|
||||
}
|
||||
},
|
||||
|
||||
dbV11: function(d, error, done) {
|
||||
d.changeVersion(d.version, '1.1', function (t) {
|
||||
t.executeSql('CREATE TABLE styles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, code TEXT NOT NULL, enabled INTEGER NOT NULL, originalCode TEXT NULL);');
|
||||
t.executeSql('CREATE TABLE style_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);');
|
||||
t.executeSql('CREATE INDEX style_meta_style_id ON style_meta (style_id);');
|
||||
}, error, function() { webSqlStorage.dbV12(d, error, done)});
|
||||
},
|
||||
|
||||
dbV12: function(d, error, done) {
|
||||
d.changeVersion(d.version, '1.2', function (t) {
|
||||
// add section table
|
||||
t.executeSql('CREATE TABLE sections (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, style_id INTEGER NOT NULL, code TEXT NOT NULL);');
|
||||
t.executeSql('INSERT INTO sections (style_id, code) SELECT id, code FROM styles;');
|
||||
// switch meta to sections
|
||||
t.executeSql('DROP INDEX style_meta_style_id;');
|
||||
t.executeSql('CREATE TABLE section_meta (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, section_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL);');
|
||||
t.executeSql('INSERT INTO section_meta (section_id, name, value) SELECT s.id, sm.name, sm.value FROM sections s INNER JOIN style_meta sm ON sm.style_id = s.style_id;');
|
||||
t.executeSql('CREATE INDEX section_meta_section_id ON section_meta (section_id);');
|
||||
t.executeSql('DROP TABLE style_meta;');
|
||||
// drop extra fields from styles table
|
||||
t.executeSql('CREATE TABLE newstyles (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, url TEXT, updateUrl TEXT, md5Url TEXT, name TEXT NOT NULL, enabled INTEGER NOT NULL);');
|
||||
t.executeSql('INSERT INTO newstyles (id, url, updateUrl, md5Url, name, enabled) SELECT id, url, updateUrl, md5Url, name, enabled FROM styles;');
|
||||
t.executeSql('DROP TABLE styles;');
|
||||
t.executeSql('ALTER TABLE newstyles RENAME TO styles;');
|
||||
}, error, function() { webSqlStorage.dbV13(d, error, done)});
|
||||
},
|
||||
|
||||
dbV13: function(d, error, done) {
|
||||
d.changeVersion(d.version, '1.3', function (t) {
|
||||
// clear out orphans
|
||||
t.executeSql('DELETE FROM section_meta WHERE section_id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);');
|
||||
t.executeSql('DELETE FROM sections WHERE id IN (SELECT sections.id FROM sections LEFT JOIN styles ON styles.id = sections.style_id WHERE styles.id IS NULL);');
|
||||
}, error, function() { webSqlStorage.dbV14(d, error, done)});
|
||||
},
|
||||
|
||||
dbV14: function(d, error, done) {
|
||||
d.changeVersion(d.version, '1.4', function (t) {
|
||||
t.executeSql('UPDATE styles SET url = null WHERE url = "undefined";');
|
||||
}, error, function() { webSqlStorage.dbV15(d, error, done)});
|
||||
},
|
||||
|
||||
dbV15: function(d, error, done) {
|
||||
d.changeVersion(d.version, '1.5', function (t) {
|
||||
t.executeSql('ALTER TABLE styles ADD COLUMN originalMd5 TEXT NULL;');
|
||||
}, error, function() { done(d); });
|
||||
}
|
||||
}
|
1138
storage.js
259
update.js
|
@ -1,115 +1,162 @@
|
|||
/* globals getStyles, saveStyle, prefs */
|
||||
/* global getStyles, saveStyle, styleSectionsEqual, chromeLocal */
|
||||
/* global getStyleDigests, updateStyleDigest */
|
||||
'use strict';
|
||||
|
||||
var update = {
|
||||
fetch: (resource, callback) => {
|
||||
let req = new XMLHttpRequest();
|
||||
let [url, data] = resource.split('?');
|
||||
req.open('POST', url, true);
|
||||
req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
|
||||
req.onload = () => callback(req.responseText);
|
||||
req.onerror = req.ontimeout = () => callback();
|
||||
req.send(data);
|
||||
// eslint-disable-next-line no-var
|
||||
var updater = {
|
||||
|
||||
COUNT: 'count',
|
||||
UPDATED: 'updated',
|
||||
SKIPPED: 'skipped',
|
||||
DONE: 'done',
|
||||
|
||||
// details for SKIPPED status
|
||||
EDITED: 'locally edited',
|
||||
MAYBE_EDITED: 'may be locally edited',
|
||||
SAME_MD5: 'up-to-date: MD5 is unchanged',
|
||||
SAME_CODE: 'up-to-date: code sections are unchanged',
|
||||
ERROR_MD5: 'error: MD5 is invalid',
|
||||
ERROR_JSON: 'error: JSON is invalid',
|
||||
|
||||
lastUpdateTime: parseInt(localStorage.lastUpdateTime) || Date.now(),
|
||||
|
||||
checkAllStyles({observer = () => {}, save = true, ignoreDigest} = {}) {
|
||||
updater.resetInterval();
|
||||
updater.checkAllStyles.running = true;
|
||||
return getStyles({}).then(styles => {
|
||||
styles = styles.filter(style => style.updateUrl);
|
||||
observer(updater.COUNT, styles.length);
|
||||
updater.log('');
|
||||
updater.log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
|
||||
return Promise.all(
|
||||
styles.map(style =>
|
||||
updater.checkStyle({style, observer, save, ignoreDigest})));
|
||||
}).then(() => {
|
||||
observer(updater.DONE);
|
||||
updater.log('');
|
||||
updater.checkAllStyles.running = false;
|
||||
});
|
||||
},
|
||||
md5Check: (style, callback, skipped) => {
|
||||
let req = new XMLHttpRequest();
|
||||
req.open('GET', style.md5Url, true);
|
||||
req.onload = () => {
|
||||
let md5 = req.responseText;
|
||||
if (md5 && md5 !== style.originalMd5) {
|
||||
callback(style);
|
||||
|
||||
checkStyle({style, observer = () => {}, save = true, ignoreDigest}) {
|
||||
let hasDigest;
|
||||
/*
|
||||
Original style digests are calculated in these cases:
|
||||
* style is installed or updated from server
|
||||
* style is checked for an update and its code is equal to the server code
|
||||
|
||||
Update check proceeds in these cases:
|
||||
* style has the original digest and it's equal to the current digest
|
||||
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
|
||||
* [ignoreDigest: none/false] style doesn't yet have the original digest
|
||||
so we compare the code to the server code and if it's the same we save the digest,
|
||||
otherwise we skip the style and report MAYBE_EDITED status
|
||||
|
||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||
*/
|
||||
return getStyleDigests(style)
|
||||
.then(maybeFetchMd5)
|
||||
.then(maybeFetchCode)
|
||||
.then(maybeSave)
|
||||
.then(saved => {
|
||||
observer(updater.UPDATED, saved);
|
||||
updater.log(updater.UPDATED + ` #${saved.id} ${saved.name}`);
|
||||
})
|
||||
.catch(err => {
|
||||
observer(updater.SKIPPED, style, err);
|
||||
err = err === 0 ? 'server unreachable' : err;
|
||||
updater.log(updater.SKIPPED + ` (${err}) #${style.id} ${style.name}`);
|
||||
});
|
||||
|
||||
function maybeFetchMd5([originalDigest, current]) {
|
||||
hasDigest = Boolean(originalDigest);
|
||||
if (hasDigest && !ignoreDigest && originalDigest != current) {
|
||||
return Promise.reject(updater.EDITED);
|
||||
}
|
||||
else {
|
||||
skipped(`"${style.name}" style is up-to-date`);
|
||||
return download(style.md5Url);
|
||||
}
|
||||
|
||||
function maybeFetchCode(md5) {
|
||||
if (!md5 || md5.length != 32) {
|
||||
return Promise.reject(updater.ERROR_MD5);
|
||||
}
|
||||
if (md5 == style.originalMd5 && hasDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.SAME_MD5);
|
||||
}
|
||||
return download(style.updateUrl);
|
||||
}
|
||||
|
||||
function maybeSave(text) {
|
||||
const json = tryJSONparse(text);
|
||||
if (!styleJSONseemsValid(json)) {
|
||||
return Promise.reject(updater.ERROR_JSON);
|
||||
}
|
||||
json.id = style.id;
|
||||
if (styleSectionsEqual(json, style)) {
|
||||
// JSONs may have different order of items even if sections are effectively equal
|
||||
// so we'll update the digest anyway
|
||||
updateStyleDigest(json);
|
||||
return Promise.reject(updater.SAME_CODE);
|
||||
} else if (!hasDigest && !ignoreDigest) {
|
||||
return Promise.reject(updater.MAYBE_EDITED);
|
||||
}
|
||||
return !save ? json :
|
||||
saveStyle(Object.assign(json, {
|
||||
name: null, // keep local name customizations
|
||||
reason: 'update',
|
||||
}));
|
||||
}
|
||||
|
||||
function styleJSONseemsValid(json) {
|
||||
return json
|
||||
&& json.sections
|
||||
&& json.sections.length
|
||||
&& typeof json.sections.every == 'function'
|
||||
&& typeof json.sections[0].code == 'string';
|
||||
}
|
||||
},
|
||||
|
||||
schedule() {
|
||||
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
|
||||
if (interval) {
|
||||
const elapsed = Math.max(0, Date.now() - updater.lastUpdateTime);
|
||||
debounce(updater.checkAllStyles, Math.max(10e3, interval - elapsed));
|
||||
} else {
|
||||
debounce.unregister(updater.checkAllStyles);
|
||||
}
|
||||
},
|
||||
|
||||
resetInterval() {
|
||||
localStorage.lastUpdateTime = updater.lastUpdateTime = Date.now();
|
||||
updater.schedule();
|
||||
},
|
||||
|
||||
log: (() => {
|
||||
let queue = [];
|
||||
let lastWriteTime = 0;
|
||||
return text => {
|
||||
queue.push({text, time: new Date().toLocaleString()});
|
||||
debounce(flushQueue, text && updater.checkAllStyles.running ? 1000 : 0);
|
||||
};
|
||||
req.onerror = req.ontimeout = () => skipped('Error validating MD5 checksum');
|
||||
req.send();
|
||||
},
|
||||
list: (callback) => {
|
||||
getStyles({}, (styles) => callback(styles.filter(style => style.updateUrl)));
|
||||
},
|
||||
perform: (observe = function () {}) => {
|
||||
// from install.js
|
||||
function arraysAreEqual (a, b) {
|
||||
// treat empty array and undefined as equivalent
|
||||
if (typeof a === 'undefined') {
|
||||
return (typeof b === 'undefined') || (b.length === 0);
|
||||
}
|
||||
if (typeof b === 'undefined') {
|
||||
return (typeof a === 'undefined') || (a.length === 0);
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.every(function (entry) {
|
||||
return b.indexOf(entry) !== -1;
|
||||
});
|
||||
}
|
||||
// from install.js
|
||||
function sectionsAreEqual(a, b) {
|
||||
if (a.code !== b.code) {
|
||||
return false;
|
||||
}
|
||||
return ['urls', 'urlPrefixes', 'domains', 'regexps'].every(function (attribute) {
|
||||
return arraysAreEqual(a[attribute], b[attribute]);
|
||||
});
|
||||
}
|
||||
|
||||
update.list(styles => {
|
||||
observe('count', styles.length);
|
||||
styles.forEach(style => update.md5Check(style, style => update.fetch(style.updateUrl, response => {
|
||||
if (response) {
|
||||
let json = JSON.parse(response);
|
||||
|
||||
if (json.sections.length === style.sections.length) {
|
||||
if (json.sections.every((section) => {
|
||||
return style.sections.some(installedSection => sectionsAreEqual(section, installedSection));
|
||||
})) {
|
||||
return observe('single-skipped', '2'); // everything is the same
|
||||
}
|
||||
json.method = 'saveStyle';
|
||||
json.id = style.id;
|
||||
|
||||
saveStyle(json).then(style => {
|
||||
observe('single-updated', style.name);
|
||||
});
|
||||
}
|
||||
else {
|
||||
return observe('single-skipped', '3'); // style sections mismatch
|
||||
function flushQueue() {
|
||||
chromeLocal.getValue('updateLog').then((lines = []) => {
|
||||
const time = Date.now() - lastWriteTime > 11e3 ? queue[0].time + ' ' : '';
|
||||
if (!queue[0].text) {
|
||||
queue.shift();
|
||||
if (lines[lines.length - 1]) {
|
||||
lines.push('');
|
||||
}
|
||||
}
|
||||
}), () => observe('single-skipped', '1')));
|
||||
});
|
||||
}
|
||||
lines.splice(0, lines.length - 1000);
|
||||
lines.push(time + queue[0].text);
|
||||
lines.push(...queue.slice(1).map(item => item.text));
|
||||
chromeLocal.setValue('updateLog', lines);
|
||||
lastWriteTime = Date.now();
|
||||
queue = [];
|
||||
});
|
||||
}
|
||||
})(),
|
||||
};
|
||||
// automatically update all user-styles if "updateInterval" pref is set
|
||||
window.setTimeout(function () {
|
||||
let id;
|
||||
function run () {
|
||||
update.perform(/*(cmd, value) => console.log(cmd, value)*/);
|
||||
reset();
|
||||
}
|
||||
function reset () {
|
||||
window.clearTimeout(id);
|
||||
let interval = prefs.get('updateInterval');
|
||||
// if interval === 0 => automatic update is disabled
|
||||
if (interval) {
|
||||
/* console.log('next update', interval); */
|
||||
id = window.setTimeout(run, interval * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
if (prefs.get('updateInterval')) {
|
||||
run();
|
||||
}
|
||||
chrome.runtime.onMessage.addListener(request => {
|
||||
// when user has changed the predefined time interval in the settings page
|
||||
if (request.method === 'prefChanged' && request.prefName === 'updateInterval') {
|
||||
reset();
|
||||
}
|
||||
// when user just manually checked for updates
|
||||
if (request.method === 'resetInterval') {
|
||||
reset();
|
||||
}
|
||||
});
|
||||
}, 10000);
|
||||
|
||||
updater.schedule();
|
||||
prefs.subscribe(updater.schedule, ['updateInterval']);
|
||||
|
|
BIN
world_go.png
Before Width: | Height: | Size: 2.5 KiB |