Compare commits

...

39 Commits

Author SHA1 Message Date
tophf
fc39f0d5a6 parserlib: dot-separated layer names 2022-10-19 22:47:19 +03:00
tophf
1725c0ecb9 show installed styles in popup finder
fixes #1488
2022-10-12 20:04:45 +03:00
tophf
79dff2775b add upDownKeyJumps option, remove left/right 2022-10-08 13:07:57 +03:00
tophf
b22bbaaec0 make <iframe> more evident 2022-10-08 13:05:40 +03:00
tophf
fff59ee12f use a self-explanatory "..." in #write-for-frames 2022-09-17 21:42:39 +03:00
tophf
540e2af62c micro-optimize colorConverter.parse
#hex, named colors: 15x faster
rgb() and other functions: 1.6x faster
2022-09-17 20:10:18 +03:00
tophf
0128489bbb unbork rgb colors with % 2022-09-17 01:29:11 +03:00
dependabot[bot]
a5b11ac687
Bump node-forge and web-ext (#1474) 2022-09-16 16:24:27 +03:00
Rob Garrison
5d3a9dccf3 1.5.28 2022-09-16 08:11:25 -05:00
tophf
09691a6362 add a comment for USO devs 2022-09-16 15:33:11 +03:00
tophf
62987fe5f8 update locales 2022-09-16 12:47:55 +03:00
tophf
405a93f9e5 no need for fse dep here 2022-09-16 12:47:37 +03:00
tophf
da4bdc6821 API should return something 2022-09-16 12:40:14 +03:00
tophf
d1f5468a81 fix USO install button 2022-09-16 12:39:46 +03:00
tophf
efc6d09d49 properly show "no styles found" 2022-09-15 17:14:13 +03:00
tophf
0bb0d32c29 simplify fitNameColumn
as it wasn't using average and an average is too small anyway
2022-09-15 13:05:00 +03:00
tophf
f5397b8aec simplify hover highlight to an underline 2022-09-15 12:57:01 +03:00
tophf
1594b4dcd8 help auto-hyphenation at word boundaries 2022-09-15 12:54:26 +03:00
tophf
527d7c0fbc add fake header before linking to USW 2022-09-11 20:44:19 +03:00
tophf
cda606e7cc tall applies-to toggle + simplify CSS 2022-09-08 00:20:25 +03:00
tophf
5cb30c8b69 simplify oldUI CSS 2022-09-07 23:57:19 +03:00
tophf
1b914a397e simplify hover animation: 50% less CPU 2022-09-06 21:25:13 +03:00
tophf
7b64deeb37 create templates on demand 2022-09-06 18:41:22 +03:00
tophf
9398857d93 clear searchMode along with search 2022-09-06 01:07:14 +03:00
tophf
b45825c015 preserve current URL params in openManage 2022-09-06 00:54:52 +03:00
tophf
3aae3f181a linkify numbers in option labels 2022-09-05 23:08:03 +03:00
tophf
48d90544f6 reuse setInputValue 2022-09-05 22:41:17 +03:00
tophf
ef998e423e add multi-column mode option 2022-09-04 22:50:27 +03:00
tophf
6979958908 use localization cache 2022-09-04 22:50:26 +03:00
tophf
4236eb4e29 shorten highlight animation
...because it was invisible 90% of time anyway
2022-09-04 22:50:26 +03:00
tophf
c351413c3f show Size column + simplify sorter 2022-09-03 20:08:24 +03:00
tophf
d91cf11366 parserlib: update forced-color-adjust 2022-09-02 09:12:32 +03:00
tophf
e49644f1c8 CodeMirror 5.65.8 2022-09-01 11:35:37 +03:00
tophf
15b59ae207 update locales 2022-09-01 11:35:37 +03:00
tophf
ad43560016 fix and simplify applyScrollInfo 2022-09-01 11:35:24 +03:00
tophf
c423025c5d postpone lazyScripts more 2022-09-01 10:37:29 +03:00
tophf
030621462c error.stack has no message in FF 2022-09-01 10:30:46 +03:00
tophf
bf3dd0318d don't apply global section to Stylus pages
unless it was intentionally targeted via url(),  url-prefix(), or regexp(). The regexp must contain the word "extension" without quotes.
2022-08-31 16:27:31 +03:00
tophf
3489b513c9 [autocomplete] find LESS vars 2022-08-30 10:31:21 +03:00
61 changed files with 3701 additions and 6054 deletions

View File

@ -346,6 +346,9 @@
"genericSavedMessage": {
"message": "Gespeichert"
},
"genericSize": {
"message": "Größe"
},
"genericTitle": {
"message": "Name"
},
@ -543,6 +546,9 @@
"manageMaxTargets": {
"message": "Anzahl der \"Gilt für\" Elemente"
},
"manageMinColumnWidth": {
"message": "Minimale Spaltenbreite (in Pixeln. 9999 deaktiviert den Mehrspalten-Modus)"
},
"manageNewStyleAsUsercss": {
"message": "als UserCSS"
},
@ -1115,7 +1121,7 @@
"message": "Wöchentliche Installationen"
},
"searchStyleQueryHint": {
"message": "Stylenamen ohne Beachtung der Groß-/Kleinschreibung suchen:\nMehrere Suchworte - alle Wörter in beliebiger Reihenfolge\n\"Bestimmte Phrase\" - genau diese Phrase ohne Anführungszeichen\n2020 - zeigt auch Styles, die 2020 aktualisiert wurden"
"message": "Stylenamen durchsuchen (Groß-Kleinschreibung wird beachtet, sobald ein Großbuchstabe benutzt wird):\nHaus Baum Sonne - sucht nach allen Wörtern in beliebiger Reihenfolge\n\"Baum Haus\" - sucht exakt diesen Ausdruck (ohne Anführungszeichen)\n/foo.*bar/i - Regulärer Ausdruck ohne Leerzeichen (nutze stattdessen \\s)"
},
"searchStylesAll": {
"message": "Alles"

View File

@ -186,6 +186,10 @@
"message": "Theme",
"description": "Label for the style editor's CSS theme."
},
"cm_arrowKeysTraverse": {
"message": "Arrow keys ↑↓ traverse sections",
"description": "Label for the option in the editor."
},
"colorpickerPaletteHint": {
"message": "Right-click a swatch to cycle through its source lines"
},
@ -498,6 +502,9 @@
"message": "Saved",
"description": "Used in various parts of the UI to indicate that something was saved"
},
"genericSize": {
"message": "Size"
},
"genericTest": {
"message": "Test",
"description": "Label for the action that runs some test e.g. opens the regexp tester panel in the editor"
@ -759,6 +766,9 @@
"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"
},
"manageMinColumnWidth": {
"message": "Minimum column width (in pixels; 9999 disables multi-column mode)"
},
"manageNewStyleAsUsercss": {
"message": "as Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"

View File

@ -1143,9 +1143,6 @@
"searchResultWeeklyCount": {
"message": "Instalaciones semanales"
},
"searchStyleQueryHint": {
"message": "La búsqueda de nombres de estilo no distingue mayúsculas de minúsculas:\nalgunas palabras: todas las palabras en cualquier orden\n\"una frase\": esta frase exacta (sin comillas)\n2020: al escribir un año, se muestran estilos que se actualizaron ese año"
},
"searchStylesAll": {
"message": "Todos"
},

View File

@ -1051,9 +1051,6 @@
"searchResultWeeklyCount": {
"message": "Instalaciones semanales"
},
"searchStyleQueryHint": {
"message": "La búsqueda de nombres de estilo no distingue mayúsculas de minúsculas:\nalgunas palabras: todas las palabras en cualquier orden\n\"una frase\": esta frase exacta (sin comillas)\n2020: al escribir un año, se muestran estilos que se actualizaron ese año"
},
"searchStylesAll": {
"message": "Todos"
},

View File

@ -355,6 +355,9 @@
"genericSavedMessage": {
"message": "保存しました"
},
"genericSize": {
"message": "サイズ"
},
"genericTest": {
"message": "テスト"
},
@ -558,6 +561,9 @@
"manageMaxTargets": {
"message": "適用先欄の表示件数"
},
"manageMinColumnWidth": {
"message": "最小カラム幅 (ピクセル単位。9999にすると、マルチカラムモードを無効にします)"
},
"manageNewStyleAsUsercss": {
"message": "Usercssとして"
},
@ -1139,7 +1145,7 @@
"message": "週間インストール数"
},
"searchStyleQueryHint": {
"message": "スタイル名を大文字小文字の区別なく検索します:\n複数の単語 - 全単語を順番の区別なく検索します\n\"複数の単語からなるフレーズ\" - 正確なフレーズを (引用符は除いて) 検索します\n2020 - このように数値 (年) を指定すると、2020年に更新されたスタイルも結果に表示します"
"message": "スタイル名を検索します (大文字を使用すると、大文字小文字を区別して検索します) :\n複数の単語 - すべての単語を (順不同で) 含む\n\"複数の単語からなるフレーズ\" - 正確なフレーズを (引用符は除いて) 探す\n/foo.*bar/i - 正規表現 (空白は使用不可。空白を使いたい場合は、代わりに \\s を使ってください)"
},
"searchStylesAll": {
"message": "すべて"

View File

@ -1058,9 +1058,6 @@
"searchResultWeeklyCount": {
"message": "주간 설치 수"
},
"searchStyleQueryHint": {
"message": "대소문자와 무관하게 스타일 이름을 검색하십시오:\n이런 검색어 - 순서에 무관하게 모든 단어가 포함하기만 하면 스타일을 불러옴\n\"이런 검색어\" - 따옴표 속 정확한 문장을 포함하는 스타일을 불러옴\n2020 - 해당 연도, 여기서는 2020년에 갱신된 스타일을 불러옴"
},
"searchStylesAll": {
"message": "모두"
},

View File

@ -62,7 +62,7 @@
"message": "Alle updates toepassen"
},
"author": {
"message": "Auteur"
"message": "Maker"
},
"backupButtons": {
"message": "Back-up"
@ -98,7 +98,7 @@
"message": "Automatisch aanvullen tijdens typen"
},
"cm_colorpicker": {
"message": "Kleurkiezer voor CSS-kleuren"
"message": "Kleurkiezers voor CSS-kleuren"
},
"cm_indentWithTabs": {
"message": "Tabs met slimme inspringing gebruiken"
@ -371,6 +371,9 @@
"genericSavedMessage": {
"message": "Opgeslagen"
},
"genericSize": {
"message": "Grootte"
},
"genericTest": {
"message": "Testen"
},
@ -568,6 +571,9 @@
"manageMaxTargets": {
"message": "Aantal Van toepassing op-items"
},
"manageMinColumnWidth": {
"message": "Minimale kolombreedte (in pixels; 9999 schakelt de modus met meerdere kolommen uit)"
},
"manageNewStyleAsUsercss": {
"message": "als Usercss"
},
@ -1146,7 +1152,7 @@
"message": "Wekelijks aantal installaties"
},
"searchStyleQueryHint": {
"message": "Stijlnamen niet hoofdlettergevoelig doorzoeken:\nbepaalde woorden - alle woorden in willekeurige volgorde\n\"bepaalde woordgroep\" - deze exacte woordgroep zonder aanhalingstekens\n2020 - een jaar als dit toont ook stijlen die in 2020 zijn bijgewerkt"
"message": "Stijlnamen doorzoeken (hoofdlettergevoelig als een hoofdletter wordt gebruikt):\nenkele woorden - al deze woorden in willekeurige volgorde\n\"bepaalde woordgroep\" - deze exacte woordgroep zonder aanhalingstekens\n/foo.*bar/i - reguliere expressie zonder spaties (gebruik in plaats daarvan \\s)"
},
"searchStylesAll": {
"message": "Alles"
@ -1335,7 +1341,7 @@
"message": "Niet volledig overeenkomend, dus overgeslagen"
},
"styleRegexpTestTitle": {
"message": "Lijst van overeenkomende geopende tabbladen (klik op de URL om de focus op het tabblad te leggen)"
"message": "Lijst met overeenkomende geopende tabbladen (klik op de URL om de focus op het tabblad te leggen)"
},
"styleSaveLabel": {
"message": "Opslaan"

View File

@ -393,6 +393,9 @@
"genericSavedMessage": {
"message": "Zapisano"
},
"genericSize": {
"message": "Rozmiar"
},
"genericTest": {
"message": "Testuj"
},
@ -593,6 +596,9 @@
"manageMaxTargets": {
"message": "Liczba dotyczących elementów "
},
"manageMinColumnWidth": {
"message": "Minimalna szerokość kolumny (w pikselach; 9999 wyłącza tryb wielokolumnowy)"
},
"manageNewStyleAsUsercss": {
"message": "jako Usercss"
},
@ -1177,7 +1183,7 @@
"message": "Tygodniowa liczba instalacji"
},
"searchStyleQueryHint": {
"message": "Szukaj nazw stylów bez rozróżniania wielkości liter:\njakieś słowa - wszystkie słowa w dowolnej kolejności\n\"jakieś zdanie\" - dokładnie to zdanie bez cudzysłowów\n2020 - rok jak ten pokazuje również style zaktualizowane w 2020"
"message": "Szukaj nazwy stylów (z uwzględnieniem wielkości liter, jeśli używana jest wielka litera):\njakieś słowa wszystkie te słowa w dowolnej kolejności\n\"jakaś fraza\" ta fraza bez cudzysłowów\n/foo.*bar/i wyrażenie regularne bez spacji (zamiast tego użyj \\s)"
},
"searchStylesAll": {
"message": "Wszystkie"

View File

@ -1173,9 +1173,6 @@
"searchResultWeeklyCount": {
"message": "Загрузок за неделю"
},
"searchStyleQueryHint": {
"message": "Поиск стилистических имён по падежам:\nнекоторые слова - все слова в любом порядке\n\"какая-то фраза\" - именно эта фраза без кавычек\n2020 - такой год также показывает стили, обновлённые в 2020 году"
},
"searchStylesAll": {
"message": "Все"
},

379
_locales/vi/messages.json Normal file
View File

@ -0,0 +1,379 @@
{
"InaccessibleFileHint": {
"message": "Stylus không thể truy cập một số kiểu tập tin (chẳng hạn như PDF và JSON)."
},
"addStyleLabel": {
"message": "Viết bảng định kiểu mới"
},
"addStyleTitle": {
"message": "Thêm bảng định kiểu"
},
"alphaChannel": {
"message": "Độ mờ"
},
"appliesAdd": {
"message": "Thêm"
},
"appliesDisplay": {
"message": "Áp dụng với: $applies$",
"placeholders": {
"applies": {
"content": "$1"
}
}
},
"appliesDisplayTruncatedSuffix": {
"message": "và một số khác"
},
"appliesDomainOption": {
"message": "Các địa chỉ thuộc tên miền này"
},
"appliesHelp": {
"message": "Dùng tuỳ chọn \"Áp dụng với\" để giới hạn các địa chỉ cho đoạn mã này"
},
"appliesLabel": {
"message": "Áp dụng với"
},
"appliesLineWidgetLabel": {
"message": "Hiển thị thông tin \"Áp dụng với\""
},
"appliesLineWidgetWarning": {
"message": "Không hoạt động với CSS tối giản"
},
"appliesRegexpOption": {
"message": "URL khớp với biểu thức chính quy"
},
"appliesRemove": {
"message": "Xoá"
},
"appliesRemoveError": {
"message": "Không thể xoá mục \"Áp dụng với\" cuối cùng"
},
"appliesSpecify": {
"message": "Chỉ định"
},
"appliesToEverything": {
"message": "Tất cả"
},
"appliesUrlPrefixOption": {
"message": "URL bắt đầu bằng"
},
"author": {
"message": "Tác giả"
},
"backupButtons": {
"message": "Sao lưu"
},
"backupMessage": {
"message": "Để nhập tập tin sao lưu, kéo và thả nó vào trang này hoặc nhấp vào nút Nhập.\n\nĐể xuất một bản sao lưu tương thích với Stylus trước phiên bản 1.5.18, nhấp chuột phải hoặc nhấn giữ Shift khi nhấp chuột trái vào nút Xuất."
},
"bckpInstStyles": {
"message": "Xuất bảng định kiểu"
},
"checkForUpdate": {
"message": "Kiểm tra bản cập nhật mới"
},
"checkingForUpdate": {
"message": "Đang kiểm tra..."
},
"clickToUninstall": {
"message": "Nhấp để huỷ kích hoạt"
},
"cm_autoCloseBrackets": {
"message": "Tự động đóng ngoặc và nháy"
},
"cm_autoCloseBracketsTooltip": {
"message": "Tự động thêm dấu đóng tương ứng khi nhập một trong các dấu (, [, {, ' và \"."
},
"cm_autocompleteOnTyping": {
"message": "Tự động hoàn thành"
},
"cm_colorpicker": {
"message": "Bộ chọn màu cho màu CSS"
},
"cm_indentWithTabs": {
"message": "Lùi đầu dòng thông minh bằng tab"
},
"cm_lineWrapping": {
"message": "Gập dòng dài"
},
"cm_linter": {
"message": "Trình phân tích cú pháp"
},
"cm_matchHighlight": {
"message": "Làm nổi"
},
"cm_matchHighlightSelection": {
"message": "Chỉ vùng được chọn"
},
"cm_matchHighlightToken": {
"message": "Token nằm dưới con trỏ văn bản"
},
"cm_selectByTokens": {
"message": "Nhấp đúp để chọn token"
},
"cm_selectByTokensTooltip": {
"message": "Ví dụ về token: .foo-bar-2 #aabbcc 0.32 !important\nKhi tắt: Chọn từ (phân tách bằng dấu câu)."
},
"cm_smartIndent": {
"message": "Lùi đầu dòng thông minh"
},
"cm_tabSize": {
"message": "Chiều rộng tab"
},
"cm_theme": {
"message": "Chủ đề"
},
"colorpickerSwitchFormatTooltip": {
"message": "Đổi định dạng: HEX → RGB → HSL.\nNhấn giữ phím Shift khi nhấp để đảo thứ tự.\nPhím tắt: PgUp và PgDn."
},
"colorpickerTooltip": {
"message": "Mở bộ chọn màu"
},
"configOnChange": {
"message": "khi thay đổi"
},
"configOnChangeTooltip": {
"message": "Tự động lưu và áp dụng"
},
"configureStyle": {
"message": "Thiết lập"
},
"configureStyleOnHomepage": {
"message": "Thiết lập trên trang chủ"
},
"confirmCancel": {
"message": "Huỷ"
},
"confirmClose": {
"message": "Đóng"
},
"confirmDefault": {
"message": "Dùng mặc định"
},
"confirmDelete": {
"message": "Xoá"
},
"confirmDiscardChanges": {
"message": "Huỷ thay đổi?"
},
"confirmNo": {
"message": "Không"
},
"confirmSave": {
"message": "Lưu"
},
"confirmStop": {
"message": "Dừng"
},
"confirmYes": {
"message": "Có"
},
"connectingDropbox": {
"message": "Đang kết nối với Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Tính năng kết nối với Dropbox chỉ khả dụng khi cài ứng dụng trực tiếp từ cửa hàng web"
},
"copied": {
"message": "Đã chép vào khay nhớ tạm"
},
"copy": {
"message": "Chép vào khay nhớ tạm"
},
"dateAbbrDay": {
"message": "$value$ ngày",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrHour": {
"message": "$value$ giờ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrMonth": {
"message": "$value$ tháng",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrYear": {
"message": "$value$ năm",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateInstalled": {
"message": "Ngày cài đặt"
},
"dateUpdated": {
"message": "Ngày cập nhật"
},
"defaultTheme": {
"message": "mặc định"
},
"deleteStyleConfirm": {
"message": "Bạn có chắc chắn muốn xoá bảng định kiểu này không?"
},
"deleteStyleLabel": {
"message": "Xoá"
},
"disableAllStyles": {
"message": "Tắt tất cả bảng định kiểu"
},
"disableAllStylesOff": {
"message": "Bật tất cả bảng định kiểu"
},
"disableStyleLabel": {
"message": "Vô hiệu hoá"
},
"draftAction": {
"message": "Chọn \"Có\" để tải bản nháp hoặc \"Không\" để xoá nó đi."
},
"editDeleteText": {
"message": "Xoá"
},
"editGotoLine": {
"message": "Đi đến dòng (hoặc dòng:cột)"
},
"editStyleHeading": {
"message": "Sửa bảng định kiểu"
},
"editStyleLabel": {
"message": "Sửa"
},
"editStyleTitle": {
"message": "Sửa bảng định kiểu $stylename$",
"placeholders": {
"stylename": {
"content": "$1"
}
}
},
"editorSettings": {
"message": "Cài đặt trình soạn thảo"
},
"enableStyleLabel": {
"message": "Kích hoạt"
},
"exportCompatible": {
"message": "Xuất (chế độ tương thích)"
},
"exportLabel": {
"message": "Xuất"
},
"exportSavedSuccess": {
"message": "Lưu thành công"
},
"externalFeedback": {
"message": "Phản hồi"
},
"externalHomepage": {
"message": "Trang chủ"
},
"externalLink": {
"message": "Liên kết ngoài"
},
"externalSupport": {
"message": "Ủng hộ"
},
"genericAdd": {
"message": "Thêm"
},
"genericDescription": {
"message": "Mô tả"
},
"genericDisabledLabel": {
"message": "Vô hiệu hoá"
},
"genericEnabledLabel": {
"message": "Kích hoạt"
},
"genericError": {
"message": "Lỗi"
},
"genericHistoryLabel": {
"message": "Lịch sử"
},
"genericNext": {
"message": "Sau"
},
"genericPrevious": {
"message": "Trước"
},
"genericResetLabel": {
"message": "Đặt lại"
},
"genericSavedMessage": {
"message": "Đã lưu"
},
"genericTest": {
"message": "Thứ"
},
"genericTitle": {
"message": "Tiêu đề"
},
"genericUnknown": {
"message": "Không xác định"
},
"helpAlt": {
"message": "Trợ giúp"
},
"importLabel": {
"message": "Nhập"
},
"importReportLegendAdded": {
"message": "đã thêm"
},
"importReportLegendIdentical": {
"message": "Đã bỏ qua các tệp trùng lặp"
},
"importReportLegendInvalid": {
"message": "Đã bỏ qua các tệp không hợp lệ"
},
"importReportLegendUpdatedBoth": {
"message": "đã cập nhật siêu thông tin và mã"
},
"importReportLegendUpdatedCode": {
"message": "đã cập nhật mã"
},
"importReportLegendUpdatedMeta": {
"message": "đã cập nhật siêu thông tin"
},
"importReportTitle": {
"message": "Đã nhập xong"
},
"installUpdateFromLabel": {
"message": "Kiểm tra bản cập nhật mới"
},
"license": {
"message": "Giấy phép"
},
"linterCSSLintIncompatible": {
"message": "CSSLint không hỗ trợ bộ tiền xử lý $preprocessorname$",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"linterJSONError": {
"message": "Định dạng JSON không hợp lệ"
},
"styleEnabledLabel": {
"message": "Kích hoạt"
},
"styleSaveLabel": {
"message": "Lưu"
}
}

View File

@ -393,6 +393,9 @@
"genericSavedMessage": {
"message": "已保存"
},
"genericSize": {
"message": "大小"
},
"genericTest": {
"message": "测试"
},
@ -1170,9 +1173,6 @@
"searchResultWeeklyCount": {
"message": "本周安装次数"
},
"searchStyleQueryHint": {
"message": "空格多词 -- 同时精确匹配(无需双引号)\n2020 -- 也含更新日期的匹配结果\n大小写不敏感"
},
"searchStylesAll": {
"message": "全部"
},
@ -1392,7 +1392,7 @@
"message": "更新 URL"
},
"stylusUnavailableForURL": {
"message": "Stylus 无法介入到此类页面"
"message": "Stylus 不能在此类页面上工作"
},
"stylusUnavailableForURLdetails": {
"message": "出于安全考虑,浏览器禁止扩展程序影响其内置页面(例如 chrome://versionChrome 61 后的标准新标签页about:addons 等等)以及其他扩展程序的页面。每个浏览器也限制了对于自己扩展程序库的介入〔例如 Chrome 网上应用店、Firefox 附加组件addons.mozilla.org。"

View File

@ -393,6 +393,9 @@
"genericSavedMessage": {
"message": "已儲存"
},
"genericSize": {
"message": "大小"
},
"genericTest": {
"message": "測試"
},
@ -593,6 +596,9 @@
"manageMaxTargets": {
"message": "已套用項目的數量"
},
"manageMinColumnWidth": {
"message": "最小欄位寬度以像素為單位9999 停用多欄模式)"
},
"manageNewStyleAsUsercss": {
"message": "做為 Usercss"
},
@ -1174,7 +1180,7 @@
"message": "每週安裝"
},
"searchStyleQueryHint": {
"message": "搜尋樣式名稱時是區分大小寫的:\nsome words - 以任意順序搜尋所有文字\n\"some phrase\" - 精確符合引號內的詞語\n2020 - 如此年份顯示在2020年更新的樣式"
"message": "搜尋樣式名稱(若使用大寫字母,則區分大小寫):\n一些字 - 以任何順序符合所有字\n\"一些詞\" - 精確符合引號內的詞\n/foo.*bar/i - 沒有空格的正規表示式(使用 \\s 代替)"
},
"searchStylesAll": {
"message": "全部"

View File

@ -8,7 +8,7 @@
/* global usercssMan */
/* global usoApi */
/* global uswApi */
/* global FIREFOX UA activateTab findExistingTab openURL */ // toolbox.js
/* global FIREFOX UA activateTab openURL */ // toolbox.js
/* global colorScheme */ // color-scheme.js
'use strict';
@ -87,28 +87,25 @@ addAPI(/** @namespace API */ {
/** @returns {Promise<chrome.tabs.Tab>} */
async openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
const setUrlParams = url => {
const u = new URL(url);
if (search) u.searchParams.set('search', search);
if (searchMode) u.searchParams.set('searchMode', searchMode);
if (options) u.hash = '#stylus-options';
return u.href;
};
const base = chrome.runtime.getURL('manage.html');
const url = setUrlParams(base);
const tabs = await browser.tabs.query({url: base + '*'});
const same = tabs.find(t => t.url === url);
let tab = same || tabs[0];
if (!tab) {
API.prefsDb.get('badFavs'); // prime the cache to avoid flicker/delay when opening the page
tab = await openURL({url, newTab: true});
} else if (!same) {
msg.sendTab(tab.id, {method: 'pushState', url: setUrlParams(tab.url)});
}
if (options) {
url += '#stylus-options';
}
const tab = await findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true,
});
if (tab) {
await activateTab(tab);
if (url !== (tab.pendingUrl || tab.url)) {
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
}
return tab;
}
API.prefsDb.get('badFavs'); // prime the cache to avoid flicker/delay when opening the page
return openURL({url, ignoreExisting: true}).then(activateTab); // activateTab unminimizes the window
return activateTab(tab); // activateTab unminimizes the window
},
/**

View File

@ -1,7 +1,7 @@
/* global API msg */// msg.js
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global bgReady createCache uuidIndex */// common.js
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
/* global calcStyleDigest styleCodeEmpty */// sections-util.js
/* global db */
/* global prefs */
/* global tabMan */
@ -74,6 +74,29 @@ const styleMan = (() => {
_rev: 0,
};
uuidIndex.addCustomId(orderWrap, {set: setOrder});
class MatchQuery {
constructor(url) {
this.url = url;
}
get urlWithoutHash() {
return this._set('urlWithoutHash', this.url.split('#', 1)[0]);
}
get urlWithoutParams() {
return this._set('urlWithoutParams', this.url.split(/[?#]/, 1)[0]);
}
get domain() {
return this._set('domain', tryURL(this.url).hostname);
}
get isOwnPage() {
return this._set('isOwnPage', this.url.startsWith(URLS.ownOrigin));
}
_set(name, value) {
Object.defineProperty(this, name, {value});
return value;
}
}
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = Promise.all([init(), prefs.ready]);
@ -176,6 +199,18 @@ const styleMan = (() => {
getOrder: () => orderWrap.value,
/** @returns {Promise<string | {[remoteId:string]: styleId}>}>} */
async getRemoteInfo(id) {
if (ready.then) await ready;
if (id) return calcRemoteId(id2style(id));
const res = {};
for (const {style} of dataMap.values()) {
const [rid, vars] = calcRemoteId(style);
if (rid) res[rid] = [style.id, vars];
}
return res;
},
/** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) {
if (ready.then) await ready;
@ -226,7 +261,7 @@ const styleMan = (() => {
const styles = id
? [id2style(id)].filter(Boolean)
: getAllAsArray();
const query = createMatchQuery(url);
const query = new MatchQuery(url);
for (const style of styles) {
let excluded = false;
let excludedScheme = false;
@ -248,10 +283,7 @@ const styleMan = (() => {
excludedScheme = true;
}
for (const section of style.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
const match = urlMatchSection(query, section, true);
if (match) {
if (match === 'sloppy') {
sloppy = true;
@ -346,6 +378,17 @@ const styleMan = (() => {
return id2style(uuidIndex.get(uuid));
}
function calcRemoteId({md5Url, updateUrl, usercssData: ucd} = {}) {
let id;
id = (id = /\d+/.test(md5Url) || URLS.extractUsoArchiveId(updateUrl)) && `uso-${id}`
|| (id = URLS.extractUSwId(updateUrl)) && `usw-${id}`
|| '';
return id && [
id,
ucd && !isEmptyObj(ucd.vars),
];
}
/** @returns {StyleObj} */
function createNewStyle() {
return /** @namespace StyleObj */ {
@ -431,7 +474,7 @@ const styleMan = (() => {
cache.maybeMatch.add(id);
continue;
}
const code = getAppliedCode(createMatchQuery(url), style);
const code = getAppliedCode(new MatchQuery(url), style);
if (code) {
updated.add(url);
buildCacheEntry(cache, style, code);
@ -601,40 +644,56 @@ const styleMan = (() => {
return true;
}
function urlMatchSection(query, section) {
function urlMatchSection(query, section, skipEmptyGlobal) {
let dd, ddL, pp, ppL, rr, rrL, uu, uuL;
if (
section.domains &&
section.domains.some(d => d === query.domain || query.domain.endsWith(`.${d}`))
(dd = section.domains) && (ddL = dd.length) && dd.some(urlMatchDomain, query) ||
(pp = section.urlPrefixes) && (ppL = pp.length) && pp.some(urlMatchPrefix, query) ||
/* Per the specification the fragment portion is ignored in @-moz-document:
https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
but the spec is outdated and doesn't account for SPA sites,
so we only respect it for `url()` function */
(uu = section.urls) && (uuL = uu.length) && (
uu.includes(query.url) ||
uu.includes(query.urlWithoutHash)
) ||
(rr = section.regexps) && (rrL = rr.length) && rr.some(urlMatchRegexp, query)
) {
return true;
}
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.url.startsWith(p))) {
return true;
}
// as per spec the fragment portion is ignored in @-moz-document:
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
// but the spec is outdated and doesn't account for SPA sites
// so we only respect it for `url()` function
if (section.urls && (
section.urls.includes(query.url) ||
section.urls.includes(query.urlWithoutHash)
)) {
return true;
}
if (section.regexps && section.regexps.some(r => compileRe(r).test(query.url))) {
return true;
}
/*
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.
*/
if (section.regexps && section.regexps.some(r => compileSloppyRe(r).test(query.url))) {
if (rrL && rr.some(urlMatchRegexpSloppy, query)) {
return 'sloppy';
}
// TODO: check for invalid regexps?
return styleSectionGlobal(section);
return !rrL && !ppL && !uuL && !ddL &&
!query.isOwnPage && // We allow only intentionally targeted sections for own pages
(!skipEmptyGlobal || !styleCodeEmpty(section.code));
}
/** @this {MatchQuery} */
function urlMatchDomain(d) {
const _d = this.domain;
return d === _d ||
_d[_d.length - d.length - 1] === '.' && _d.endsWith(d);
}
/** @this {MatchQuery} */
function urlMatchPrefix(p) {
return p && this.url.startsWith(p);
}
/** @this {MatchQuery} */
function urlMatchRegexp(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileRe(r).test(this.url);
}
/** @this {MatchQuery} */
function urlMatchRegexpSloppy(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileSloppyRe(r).test(this.url);
}
function createCompiler(compile) {
@ -674,37 +733,8 @@ const styleMan = (() => {
'$';
}
function createMatchQuery(url) {
let urlWithoutHash;
let urlWithoutParams;
let domain;
return {
url,
get urlWithoutHash() {
if (!urlWithoutHash) {
urlWithoutHash = url.split('#')[0];
}
return urlWithoutHash;
},
get urlWithoutParams() {
if (!urlWithoutParams) {
const u = tryURL(url);
urlWithoutParams = u.origin + u.pathname;
}
return urlWithoutParams;
},
get domain() {
if (!domain) {
const u = tryURL(url);
domain = u.hostname;
}
return domain;
},
};
}
function buildCache(cache, url, styleList) {
const query = createMatchQuery(url);
const query = new MatchQuery(url);
for (const {style, appliesTo, preview} of styleList) {
const code = getAppliedCode(query, preview || style);
if (code) {

View File

@ -71,9 +71,10 @@ const usoApi = {};
return true;
};
function ping(id, resolve) {
return fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`)
.then(resolve);
async function ping(id, resolve) {
await fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`);
if (resolve) resolve(true);
return true;
}
function makeKey(key, {badKeys, newKeys}) {

View File

@ -35,7 +35,7 @@ const uswApi = (() => {
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
return [
'/* ==UserStyle==',
...meta.map(([k, v]) => `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v || ''}`),
...meta.map(([k, v]) => v && `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v}`).filter(Boolean),
'==/UserStyle== */',
].join('\n') + '\n\n';
}
@ -77,14 +77,15 @@ const uswApi = (() => {
*/
async publish(id, sourceCode) {
const style = await API.styles.get(id);
const code = style.usercssData ? sourceCode
: fakeUsercssHeader(style) + sourceCode;
const data = (style._usw || {}).token
? style._usw
: await linkStyle(style, sourceCode);
const header = style.usercssData ? '' : fakeUsercssHeader(style);
: await linkStyle(style, code);
return uswFetch(`style/${data.id}`, data.token, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code: header + sourceCode}),
body: JSON.stringify({code}),
});
},

View File

@ -10,13 +10,12 @@
const USO = 'https://userstyles.org';
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
const CLICK = {
customize: '.customize_button',
install: '#install_style_button',
uninstall: '#uninstall_style_button',
update: '#update_style_button',
};
const CLICK_SEL = Object.values(CLICK).join(',');
const CLICK = [
['#install_stylish_style_button', onInstall],
['#update_stylish_style_button', onInstall],
['.customize_style_button', onCustomize],
['.uninstall_stylish_style_button', onUninstall],
];
const pageEventId = `${performance.now()}${Math.random()}`;
const contentEventId = pageEventId + ':';
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
@ -43,7 +42,7 @@
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
]);
if (!dup.id) {
if (!dup) {
sendStylishEvent('styleCanBeInstalledChrome');
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
@ -53,38 +52,47 @@
}
async function onClick(e) {
const el = e.target.closest(CLICK_SEL);
if (!el) return;
el.disabled = true;
const {id} = dup;
try {
if (el.matches(CLICK.uninstall)) {
dup = style = false;
removeEventListener('change', onChange);
await API.styles.delete(id);
return;
for (const [sel, fn] of CLICK) {
const el = e.target.closest(sel);
if (!el) continue;
try {
el.disabled = true;
await fn(e);
} catch (e) {
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
} finally {
el.disabled = false;
}
if (el.matches(CLICK.customize)) {
const isOn = dup && !$('#style-settings');
toggleListener(isOn, 'change', onChange);
observeColors(isOn);
return;
}
e.stopPropagation();
if (!style) await buildStyle();
style = dup = await API.usercss.install(style, {
dup: {id},
vars: getPageVars(),
});
sendStylishEvent('styleInstalledChrome');
API.uso.pingback(id);
} catch (e) {
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
} finally {
el.disabled = false;
}
}
function onCustomize() {
const ss = $('#style-settings');
const willShow = !ss || !ss.offsetHeight;
observeColors(willShow);
toggleListener(willShow, 'change', onChange);
}
async function onInstall(e) {
const {id} = dup;
e.stopPropagation();
if (!style) await buildStyle();
style = dup = await API.usercss.install(style, {
dup: {id},
vars: getPageVars(),
});
sendStylishEvent('styleInstalledChrome');
API.uso.pingback(id);
}
function onUninstall() {
const {id} = dup;
dup = style = false;
observeColors(false);
removeEventListener('change', onChange);
return API.styles.delete(id);
}
function onChange({target: el}) {
if (dup && el.matches('[name^="ik-"], [type=file]')) {
API.usercss.configVars(dup.id, getPageVars());
@ -121,7 +129,7 @@
const {vars} = (style || dup).usercssData;
for (const el of document.querySelectorAll('[name^="ik-"]')) {
const name = el.name.slice(3); // dropping "ik-"
const ik = badKeys[name] || name;
const ik = (badKeys || {})[name] || name;
const v = vars[ik] || false;
const isImage = el.type === 'radio';
if (v && (!isImage || el.checked)) {
@ -185,45 +193,56 @@
})();
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
let done, orphaned, vars;
// `chrome` may be empty if no extensions use externally_connectable but USO needs it
if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
const {defineProperty} = Object;
const {dispatchEvent, CustomEvent, removeEventListener} = window;
const apply = Map.call.bind(Map.apply);
const CR = chrome.runtime;
const SEND = 'sendMessage';
const RP = Response.prototype;
const ORIG = {json: RP.json, [SEND]: CR[SEND]};
let done, orphaned, vars;
CR[SEND] = ovrSend;
RP.json = ovrJson;
const OVR = [
[chrome.runtime, 'sendMessage', (fn, me, args) => {
const [id, /*msg*/, opts, cb = opts] = args;
if (id !== EXT_ID) return apply(fn, me, args);
if (typeof cb !== 'function') return Promise.resolve(true);
cb(true);
}],
[Response.prototype, 'json', async (fn, me, args) => {
const res = await apply(fn, me, args);
try {
if (!done && me.url === apiUrl) {
done = true;
send(res);
setVars(res);
}
} catch (e) {}
return res;
}],
[window, 'fetch', (fn, me, args) =>
args[0] === `chrome-extension://${EXT_ID}/index.html`
? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
: apply(fn, me, args),
],
];
OVR.forEach(([obj, name, caller], i) => {
const orig = obj[name];
const ovr = new Proxy(orig, {
apply(fn, me, args) {
if (orphaned) restore(obj, name, ovr, fn);
return (orphaned ? apply : caller)(fn, me, args);
},
});
defineProperty(obj, name, {value: ovr});
OVR[i] = [obj, name, ovr, orig]; // same args as restore()
});
/* We set `isInstalled` at page start intentionally not trying to replicate Stylish login events.
* This difference allows USO site to detect presence of Stylus (or another similar extension). */
window.isInstalled = true;
addEventListener(eventId, onCommand, true);
function ovrSend(id, msg, opts, cb = opts) {
if (!orphaned &&
id === 'fjnbnpbmkenffdnngjfgmeleoegfcffe' &&
msg && msg.type === 'deleteStyle' &&
typeof cb === 'function') {
cb(true);
} else {
return ORIG[SEND](...arguments);
}
}
async function ovrJson() {
const res = await apply(ORIG.json, this, arguments);
try {
if (!done && this.url === apiUrl) {
if (RP.json === ovrJson) RP.json = ORIG.json;
done = true;
send(res);
setVars(res);
}
} catch (e) {}
return res;
}
function onCommand(e) {
if (e.detail === 'quit') {
removeEventListener(eventId, onCommand, true);
// We can restore the hooks only if another script didn't modify them
if (CR[SEND] === ovrSend) CR[SEND] = ovrSend;
if (RP.json === ovrJson) RP.json = ORIG.json;
OVR.forEach(restore);
done = orphaned = true;
} else if (/^vars:/.test(e.detail)) {
vars = JSON.parse(e.detail.slice(5));
@ -231,6 +250,11 @@ function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
send(e.relatedTarget.uploadedData);
}
}
function restore(obj, name, ovr, orig) { // same order as OVR after patching
if (obj[name] === ovr) {
defineProperty(obj, name, {value: orig});
}
}
function send(data) {
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
}
@ -261,7 +285,7 @@ function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
if (ss.setting_type === 'image') {
let isListed;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.value === value);
isListed |= opt.default = (opt.install_key === value);
}
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {

View File

@ -338,6 +338,12 @@
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option sectioned-only">
<label i18n="cm_arrowKeysTraverse">
<input id="editor.arrowKeysTraverse" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n="cm_colorpicker">
<input id="editor.colorpicker" type="checkbox">

View File

@ -41,6 +41,7 @@
const {line, ch} = pos;
const {styles, text} = cm.getLineHandle(line);
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
const isLessLang = cm.doc.mode.helperType === 'less';
const isStylusLang = cm.doc.mode.name === 'stylus';
const type = style && style.split(' ', 1)[0] || 'prop?';
if (!type || type === 'comment' || type === 'string') {
@ -86,6 +87,7 @@
'@supports',
'@viewport',
];
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
break;
case '#': // prevents autocomplete for #hex colors
@ -196,13 +198,15 @@
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
}
function findAllCssVars(cm, leftPart) {
function findAllCssVars(cm, leftPart, rightPart = '') {
// simplified regex without CSS escapes
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
const rx = new RegExp(
'(?:^|[\\s/;{])(' +
(leftPart.startsWith('--') ? leftPart : '--') +
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
(prefixed ? leftPart : '--') +
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
rightPart,
'g');
const list = new Set();
cm.eachLine(({text}) => {

View File

@ -1,4 +1,4 @@
/* global $$ $ $create messageBoxProxy setupLivePrefs */// dom.js
/* global $$ $ $create messageBoxProxy setInputValue setupLivePrefs */// dom.js
/* global API */// msg.js
/* global CODEMIRROR_THEMES */
/* global CodeMirror */
@ -136,15 +136,8 @@ function EditorHeader() {
});
resetEl.hidden = !editor.style.customName;
resetEl.onclick = () => {
const {style} = editor;
nameEl.focus();
nameEl.select();
// trying to make it undoable via Ctrl-Z
if (!document.execCommand('insertText', false, style.name)) {
nameEl.value = style.name;
editor.updateName(true);
}
style.customName = null; // to delete it from db
editor.style.customName = null; // to delete it from db
setInputValue(nameEl, editor.style.name);
resetEl.hidden = true;
};
const enabledEl = $('#enabled');

View File

@ -67,6 +67,7 @@
k.slice('editor.'.length);
const prefKeys = prefs.knownKeys.filter(k =>
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
k !== 'editor.arrowKeysTraverse' && // handled in sections-editor.js
prefToCmOpt(k) in CodeMirror.defaults);
const {insertTab, insertSoftTab} = CodeMirror.commands;

View File

@ -75,8 +75,8 @@ html:not(.is-new-style) #heading::before {
right: 24px;
}
/************ checkbox & select************/
.options-column > div[class="option"] {
margin-bottom: 4px;
.options-column > .option {
margin-bottom: .25rem;
}
.options-column > .usercss-only {
@ -107,6 +107,8 @@ label {
}
#sections {
padding-left: var(--header-width);
}
.usercss #sections {
min-height: 0;
height: 100%;
}

View File

@ -4,7 +4,7 @@
/* global SectionsEditor */
/* global SourceEditor */
/* global clipString createHotkeyInput helpPopup */// util.js
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
/* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js
/* global cmFactory */
/* global editor EditorHeader */// base.js
/* global linterMan */
@ -207,11 +207,10 @@ function EditorMethods() {
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
if (si && si.sel) {
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
cm.operation(() => {
cm.setSelections(...si.sel, {scroll: false});
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
});
cm.setSelections(...si.sel, {scroll: false});
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
Object.assign(cm.display.scroller, si.scroll); // for source editor
Object.assign(cm.doc, si.scroll); // for sectioned editor
}
},
@ -223,6 +222,7 @@ function EditorMethods() {
focus: cm.hasFocus(),
height: cm.display.wrapper.style.height.replace('100vh', ''),
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
scroll: mapObj(cm.doc, null, ['scrollLeft', 'scrollTop']),
sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex],
})),
};

View File

@ -79,8 +79,8 @@
return collectStylelintResults(res, opts);
} catch (e) {
const fatal = pass === -1 ||
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e.stack) ||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(e.stack);
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
if (fatal) {
return [{
from: {line: e.line - 1, ch: e.column - 1},

View File

@ -1,4 +1,4 @@
/* global $ $$ $create $remove focusAccessibility toggleDataset */// dom.js
/* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
/* global CodeMirror */
/* global chromeLocal */// storage-util.js
/* global colorMimicry */
@ -930,18 +930,5 @@
})));
}
function setInputValue(input, value) {
input.focus();
input.select();
// using execCommand to add to the input's undo history
document.execCommand(value ? 'insertText' : 'delete', false, value);
// some versions of Firefox ignore execCommand
if (input.value !== value) {
input.value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
}
}
//endregion
})();

View File

@ -31,6 +31,7 @@ function createSection(originalSection, genId, si) {
value: originalSection.code,
});
el.CodeMirror = cm; // used by getAssociatedEditor
cm.el = el;
editor.applyScrollInfo(cm, si);
const changeListeners = new Set();
@ -114,49 +115,10 @@ function createSection(originalSection, genId, si) {
changeGeneration = newGeneration;
emitSectionChange('code');
});
cm.display.wrapper.on('keydown', event => handleKeydown(cm, event), true);
$('.test-regexp', el).onclick = () => updateRegexpTester(true);
initBeautifyButton($('.beautify-section', el), [cm]);
}
function handleKeydown(cm, event) {
if (event.shiftKey || event.altKey || event.metaKey) {
return;
}
const {key} = event;
const {line, ch} = cm.getCursor();
switch (key) {
case 'ArrowLeft':
if (line || ch) {
return;
}
// fallthrough
case 'ArrowUp':
cm = line === 0 && editor.prevEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
break;
case 'ArrowRight':
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
return;
}
// fallthrough
case 'ArrowDown':
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(0, 0);
break;
}
}
async function updateRegexpTester(toggle) {
const isLoaded = typeof regexpTester === 'object' ||
toggle && await require(['/edit/regexp-tester']); /* global regexpTester */

View File

@ -6,6 +6,7 @@
/* global createSection */// sections-editor-section.js
/* global editor */
/* global linterMan */
/* global prefs */
/* global styleSectionsEqual */ // sections-util.js
/* global t */// localization.js
'use strict';
@ -21,6 +22,7 @@ function SectionsEditor() {
let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height
let cmExtrasHeight; // resize grip + borders
let upDownJumps;
updateMeta();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
@ -30,6 +32,10 @@ function SectionsEditor() {
$('#from-mozilla').on('click', () => showMozillaFormatImport());
document.on('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
prefs.subscribe('editor.arrowKeysTraverse', (_, val) => {
for (const {cm} of sections) handleKeydownSetup(cm, val);
upDownJumps = val;
}, {runNow: true});
/** @namespace Editor */
Object.assign(editor, {
@ -70,15 +76,15 @@ function SectionsEditor() {
}
},
nextEditor(cm, cycle = true) {
return cycle || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1)
nextEditor(cm, upDown) {
return !upDown || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1, upDown)
: null;
},
prevEditor(cm, cycle = true) {
return cycle || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1)
prevEditor(cm, upDown) {
return !upDown || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1, upDown)
: null;
},
@ -112,14 +118,16 @@ function SectionsEditor() {
editor.useSavedStyle(newStyle);
},
scrollToEditor(cm) {
const {el} = sections.find(s => s.cm === cm);
const r = el.getBoundingClientRect();
const h = window.innerHeight;
if (r.bottom > h && r.top > 0 ||
r.bottom < h && r.top < 0) {
window.scrollBy(0, (r.top + r.bottom - h) / 2 | 0);
}
scrollToEditor(cm, partial) {
const cc = partial && cm.cursorCoords(true, 'window');
const {top: y1, bottom: y2} = cm.el.getBoundingClientRect();
const rc = container.getBoundingClientRect();
const rcY1 = Math.max(rc.top, 0);
const rcY2 = Math.min(rc.bottom, innerHeight);
const bad = partial
? cc.top < rcY1 || cc.top > rcY2 - 30
: y1 >= rcY1 ^ y2 <= rcY2;
if (bad) window.scrollBy(0, (y1 + y2 - rcY2 + rcY1) / 2 | 0);
},
});
@ -291,10 +299,36 @@ function SectionsEditor() {
}
}
function nextPrevEditor(cm, direction) {
function handleKeydown(event) {
if (event.shiftKey || event.altKey || event.metaKey ||
event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
return;
}
let pos;
let cm = this.CodeMirror;
const {line, ch} = cm.getCursor();
if (event.key === 'ArrowUp') {
cm = line === 0 && editor.prevEditor(cm, true);
pos = cm && [cm.doc.size - 1, ch];
} else {
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, true);
pos = cm && [0, 0];
}
if (cm) {
cm.setCursor(...pos);
event.preventDefault();
event.stopPropagation();
}
}
function handleKeydownSetup(cm, state) {
cm.display.wrapper[state ? 'on' : 'off']('keydown', handleKeydown, true);
}
function nextPrevEditor(cm, direction, upDown) {
const editors = editor.getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
editor.scrollToEditor(cm);
editor.scrollToEditor(cm, upDown);
cm.focus();
return cm;
}
@ -577,6 +611,9 @@ function SectionsEditor() {
cm.focus();
editor.scrollToEditor(cm);
}
if (upDownJumps) {
handleKeydownSetup(cm, true);
}
updateSectionOrder();
updateLivePreview();
section.onChange(updateLivePreview);

View File

@ -101,7 +101,6 @@ async function SourceEditor() {
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
}, {runNow: true});
editor.applyScrollInfo(cm);
cm.clearHistory();
cm.markClean();
savedGeneration = cm.changeGeneration();
@ -121,6 +120,7 @@ async function SourceEditor() {
if (!$isTextInput(document.activeElement)) {
cm.focus();
}
editor.applyScrollInfo(cm); // WARNING! Place it after all cm.XXX calls that change scroll pos
async function preprocess(style) {
const res = await API.usercss.build({

View File

@ -37,7 +37,7 @@ async function InjectionOrder(show, el, selector) {
parts.name.href = '/edit.html?id=' + style.id;
parts.name.textContent = style.name;
return Object.assign(entry.cloneNode(true), {
styleNameLowerCase: style.name.toLocaleLowerCase(),
styleNameLC: style.name.toLocaleLowerCase(),
});
}

View File

@ -1,32 +1,23 @@
'use strict';
const colorConverter = (() => {
const colorConverter = (NAMED_COLORS => {
const RXS_NUM = /\s*([+-]?(?:\d+\.?\d*|\d*\.\d+))(?:e[+-]?\d+)?/.source;
const RXS_NUM_ANGLE = `${RXS_NUM}(deg|g?rad|turn)?`;
// All groups in RXS_NUM must use ?: in order to enable \1 in RX_COLOR.rgb
const RXS_NUM = /\s*[+-]?(\.\d+|\d+(\.\d*)?)(e[+-]?\d+)?/.source.replace(/\(/g, '(?:');
const RXS_ANGLE = '(?:deg|g?rad|turn)?';
const expandRe = re => RegExp(re.source.replace(/N/g, RXS_NUM).replace(/A/g, RXS_ANGLE), 'iy');
const RX_COLOR = {
hex: /#([a-f\d]{3}(?:[a-f\d](?:[a-f\d]{2}){0,2})?)\b/iy,
hsl: new RegExp([
// num_or_angle, pct, pct [ , num_or_pct]?
`^(${RXS_NUM_ANGLE})\\s*,(${RXS_NUM}%\\s*(,|$)){2}(${RXS_NUM}%?)?\\s*$`,
// num_or_angle pct pct [ / num_or_pct]?
`^(${RXS_NUM_ANGLE})\\s+(${RXS_NUM}%\\s*(\\s|$)){2}(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
hwb: new RegExp(
// num|angle|none pct|none pct|none [ / num|pct|none ]?
`^(${RXS_NUM_ANGLE}|none)(\\s+(${RXS_NUM}%|none)){2}(\\s+|$)(/${RXS_NUM}%?|none)?\\s*$`,
'iy'),
rgb: new RegExp([
// num, num, num [ , num_or_pct]?
// pct, pct, pct [ , num_or_pct]?
`^((${RXS_NUM}\\s*(,|$)){3}|(${RXS_NUM}%\\s*(,|$)){3})(${RXS_NUM}%?)?\\s*$`,
// num num num [ / num_or_pct]?
// pct pct pct [ / num_or_pct]?
`^((${RXS_NUM}\\s*(\\s|$)){3}|(${RXS_NUM}%\\s*(\\s|$)){3})(/${RXS_NUM}%?)?\\s*$`,
].join('|'), 'iy'),
// num_or_angle, pct, pct [ , num_or_pct]?
// num_or_angle pct pct [ / num_or_pct]?
hsl: expandRe(/^NA(\s*(,N%\s*){2}(,N%?\s*)?|(\s+N%){2}\s*(\/N%?\s*)?)$/),
// num_or_angle|none pct|none pct|none [ / num_or_pct|none ]?
hwb: expandRe(/^(NA|none)(\s+(N%|none)){2}\s*(\/(N%?|none)\s*)?$/),
// num, num, num [ , num_or_pct]?
// pct, pct, pct [ , num_or_pct]?
// num num num [ / num_or_pct]?
// pct pct pct [ / num_or_pct]?
rgb: expandRe(/^N(%?)(\s*,N\1\s*,N\1\s*(,N%?\s*)?|\s+N\1\s+N\1\s*(\/N%?\s*)?)$/),
};
const ANGLE_TO_DEG = {
grad: 360 / 400,
@ -51,6 +42,7 @@ const colorConverter = (() => {
'v' in c ? 'hsv' :
'l' in c ? 'hsl' :
undefined;
let HEX;
return {
parse,
@ -64,8 +56,8 @@ const colorConverter = (() => {
snapToInt,
testAt,
ALPHA_DIGITS: 3,
NAMED_COLORS,
RX_COLOR,
// NAMED_COLORS is added below
};
function format(color = '', type = color.type, {hexUppercase, usoMode, round} = {}) {
@ -95,57 +87,71 @@ const colorConverter = (() => {
}
}
function parse(str) {
if (typeof str !== 'string') return;
str = str.trim().toLowerCase();
if (!str) return;
if (str[0] !== '#' && !str.includes('(')) {
// eslint-disable-next-line no-use-before-define
str = colorConverter.NAMED_COLORS.get(str);
if (!str) return;
function parse(s) {
if (typeof s !== 'string' || !(s = s.trim())) {
return;
} else if (s[0] === '#') {
return parseHex(s);
} else if (s.endsWith(')') && (s = s.match(/^(hwb|(hsl|rgb)a?)\(\s*([^)]+)/i))) {
return parseFunc((s[2] || s[1]).toLowerCase(), s[3]);
} else {
return colorConverter.NAMED_COLORS.get(s.toLowerCase());
}
}
if (str[0] === '#') {
if (!testAt(RX_COLOR.hex, 0, str)) {
return;
}
str = str.slice(1);
const [r, g, b, a = 255] = str.length <= 4 ?
str.match(/(.)/g).map(c => parseInt(c + c, 16)) :
str.match(/(..)/g).map(c => parseInt(c, 16));
return {
type: 'hex',
r,
g,
b,
a: a === 255 ? undefined : a / 255,
};
function initHexMap() {
HEX = Array(256).fill(-0xFFFF); // ensuring a PACKED_SMI array
for (let i = 48; i < 58; i++) HEX[i] = i - 48; // 0123456789
for (let i = 65; i < 71; i++) HEX[i] = i - 65 + 10; // ABCDEF
for (let i = 97; i < 103; i++) HEX[i] = i - 97 + 10; // abcdef
}
function parseHex(str) {
if (!HEX) initHexMap();
let r, g, b, a;
const len = str.length;
if (len === 4 || len === 5
? (r = HEX[str.charCodeAt(1)] * 0x11) >= 0 &&
(g = HEX[str.charCodeAt(2)] * 0x11) >= 0 &&
(b = HEX[str.charCodeAt(3)] * 0x11) >= 0 &&
(len < 5 || (a = HEX[str.charCodeAt(4)] * 0x11 / 255) >= 0)
: (len === 7 || len === 9) &&
(r = HEX[str.charCodeAt(1)] * 0x10 + HEX[str.charCodeAt(2)]) >= 0 &&
(g = HEX[str.charCodeAt(3)] * 0x10 + HEX[str.charCodeAt(4)]) >= 0 &&
(b = HEX[str.charCodeAt(5)] * 0x10 + HEX[str.charCodeAt(6)]) >= 0 &&
(len < 9 || (a = (HEX[str.charCodeAt(7)] * 0x10 + HEX[str.charCodeAt(8)]) / 255) >= 0)
) {
return {type: 'hex', r, g, b, a};
}
}
const [, func, type = func, value] = str.match(/^((rgb|hsl)a?|hwb)\(\s*(.*?)\s*\)|$/);
if (!func || !testAt(RX_COLOR[type], 0, value)) {
function parseFunc(type, val) {
if (!testAt(RX_COLOR[type], 0, val)) {
return;
}
const [s1, s2, s3, sA] = value.split(/\s*[,/]\s*|\s+/);
const a = isNaN(sA) ? 1 : constrain(0, 1, sA / (sA.endsWith('%') ? 100 : 1));
// Not using destructuring because it's slow
const parts = val.trim().split(/\s*[,/]\s*|\s+/);
const n1 = parseFloat(parts[0]);
const n2 = parseFloat(parts[1]);
const n3 = parseFloat(parts[2]);
const nA = parseFloat(parts[3]);
const a = isNaN(nA) ? undefined : constrain(0, 1, parts[3].endsWith('%') ? nA / 100 : nA);
if (type === 'rgb') {
const k = s1.endsWith('%') ? 2.55 : 1;
const k = parts[0].endsWith('%') ? 2.55 : 1;
return {
type,
r: constrain(0, 255, Math.round(s1 * k)),
g: constrain(0, 255, Math.round(s2 * k)),
b: constrain(0, 255, Math.round(s3 * k)),
r: constrain(0, 255, Math.round(n1 * k)),
g: constrain(0, 255, Math.round(n2 * k)),
b: constrain(0, 255, Math.round(n3 * k)),
a,
};
}
const h = constrainHue(parseFloat(s1) * (ANGLE_TO_DEG[s1.match(/\D*$/)[0]] || 1));
const n2 = constrain(0, 100, parseFloat(s2) || 0);
const n3 = constrain(0, 100, parseFloat(s3) || 0);
const h = constrainHue(n1 * (ANGLE_TO_DEG[parts[0].match(/\D*$/)[0].toLowerCase()] || 1));
const n2c = constrain(0, 100, n2 || 0);
const n3c = constrain(0, 100, n3 || 0);
return type === 'hwb'
? {type, h, w: n2, b: n3, a}
: {type, h, s: n2, l: n3, a};
? {type, h, w: n2c, b: n3c, a}
: {type, h, s: n2c, l: n3c, a};
}
function formatAlpha(a) {
@ -265,157 +271,154 @@ const colorConverter = (() => {
rx.lastIndex = index;
return rx.test(text);
}
})();
colorConverter.NAMED_COLORS = new Map([
['transparent', 'rgba(0, 0, 0, 0)'],
// CSS4 named colors
['aliceblue', '#f0f8ff'],
['antiquewhite', '#faebd7'],
['aqua', '#00ffff'],
['aquamarine', '#7fffd4'],
['azure', '#f0ffff'],
['beige', '#f5f5dc'],
['bisque', '#ffe4c4'],
['black', '#000000'],
['blanchedalmond', '#ffebcd'],
['blue', '#0000ff'],
['blueviolet', '#8a2be2'],
['brown', '#a52a2a'],
['burlywood', '#deb887'],
['cadetblue', '#5f9ea0'],
['chartreuse', '#7fff00'],
['chocolate', '#d2691e'],
['coral', '#ff7f50'],
['cornflowerblue', '#6495ed'],
['cornsilk', '#fff8dc'],
['crimson', '#dc143c'],
['cyan', '#00ffff'],
['darkblue', '#00008b'],
['darkcyan', '#008b8b'],
['darkgoldenrod', '#b8860b'],
['darkgray', '#a9a9a9'],
['darkgrey', '#a9a9a9'],
['darkgreen', '#006400'],
['darkkhaki', '#bdb76b'],
['darkmagenta', '#8b008b'],
['darkolivegreen', '#556b2f'],
['darkorange', '#ff8c00'],
['darkorchid', '#9932cc'],
['darkred', '#8b0000'],
['darksalmon', '#e9967a'],
['darkseagreen', '#8fbc8f'],
['darkslateblue', '#483d8b'],
['darkslategray', '#2f4f4f'],
['darkslategrey', '#2f4f4f'],
['darkturquoise', '#00ced1'],
['darkviolet', '#9400d3'],
['deeppink', '#ff1493'],
['deepskyblue', '#00bfff'],
['dimgray', '#696969'],
['dimgrey', '#696969'],
['dodgerblue', '#1e90ff'],
['firebrick', '#b22222'],
['floralwhite', '#fffaf0'],
['forestgreen', '#228b22'],
['fuchsia', '#ff00ff'],
['gainsboro', '#dcdcdc'],
['ghostwhite', '#f8f8ff'],
['gold', '#ffd700'],
['goldenrod', '#daa520'],
['gray', '#808080'],
['grey', '#808080'],
['green', '#008000'],
['greenyellow', '#adff2f'],
['honeydew', '#f0fff0'],
['hotpink', '#ff69b4'],
['indianred', '#cd5c5c'],
['indigo', '#4b0082'],
['ivory', '#fffff0'],
['khaki', '#f0e68c'],
['lavender', '#e6e6fa'],
['lavenderblush', '#fff0f5'],
['lawngreen', '#7cfc00'],
['lemonchiffon', '#fffacd'],
['lightblue', '#add8e6'],
['lightcoral', '#f08080'],
['lightcyan', '#e0ffff'],
['lightgoldenrodyellow', '#fafad2'],
['lightgray', '#d3d3d3'],
['lightgrey', '#d3d3d3'],
['lightgreen', '#90ee90'],
['lightpink', '#ffb6c1'],
['lightsalmon', '#ffa07a'],
['lightseagreen', '#20b2aa'],
['lightskyblue', '#87cefa'],
['lightslategray', '#778899'],
['lightslategrey', '#778899'],
['lightsteelblue', '#b0c4de'],
['lightyellow', '#ffffe0'],
['lime', '#00ff00'],
['limegreen', '#32cd32'],
['linen', '#faf0e6'],
['magenta', '#ff00ff'],
['maroon', '#800000'],
['mediumaquamarine', '#66cdaa'],
['mediumblue', '#0000cd'],
['mediumorchid', '#ba55d3'],
['mediumpurple', '#9370db'],
['mediumseagreen', '#3cb371'],
['mediumslateblue', '#7b68ee'],
['mediumspringgreen', '#00fa9a'],
['mediumturquoise', '#48d1cc'],
['mediumvioletred', '#c71585'],
['midnightblue', '#191970'],
['mintcream', '#f5fffa'],
['mistyrose', '#ffe4e1'],
['moccasin', '#ffe4b5'],
['navajowhite', '#ffdead'],
['navy', '#000080'],
['oldlace', '#fdf5e6'],
['olive', '#808000'],
['olivedrab', '#6b8e23'],
['orange', '#ffa500'],
['orangered', '#ff4500'],
['orchid', '#da70d6'],
['palegoldenrod', '#eee8aa'],
['palegreen', '#98fb98'],
['paleturquoise', '#afeeee'],
['palevioletred', '#db7093'],
['papayawhip', '#ffefd5'],
['peachpuff', '#ffdab9'],
['peru', '#cd853f'],
['pink', '#ffc0cb'],
['plum', '#dda0dd'],
['powderblue', '#b0e0e6'],
['purple', '#800080'],
['rebeccapurple', '#663399'],
['red', '#ff0000'],
['rosybrown', '#bc8f8f'],
['royalblue', '#4169e1'],
['saddlebrown', '#8b4513'],
['salmon', '#fa8072'],
['sandybrown', '#f4a460'],
['seagreen', '#2e8b57'],
['seashell', '#fff5ee'],
['sienna', '#a0522d'],
['silver', '#c0c0c0'],
['skyblue', '#87ceeb'],
['slateblue', '#6a5acd'],
['slategray', '#708090'],
['slategrey', '#708090'],
['snow', '#fffafa'],
['springgreen', '#00ff7f'],
['steelblue', '#4682b4'],
['tan', '#d2b48c'],
['teal', '#008080'],
['thistle', '#d8bfd8'],
['tomato', '#ff6347'],
['turquoise', '#40e0d0'],
['violet', '#ee82ee'],
['wheat', '#f5deb3'],
['white', '#ffffff'],
['whitesmoke', '#f5f5f5'],
['yellow', '#ffff00'],
['yellowgreen', '#9acd32'],
]);
})(new Map([
['transparent', {r: 0, g: 0, b: 0, a: 0, type: 'rgb'}],
['aliceblue', {r: 240, g: 248, b: 255, type: 'hex'}],
['antiquewhite', {r: 250, g: 235, b: 215, type: 'hex'}],
['aqua', {r: 0, g: 255, b: 255, type: 'hex'}],
['aquamarine', {r: 127, g: 255, b: 212, type: 'hex'}],
['azure', {r: 240, g: 255, b: 255, type: 'hex'}],
['beige', {r: 245, g: 245, b: 220, type: 'hex'}],
['bisque', {r: 255, g: 228, b: 196, type: 'hex'}],
['black', {r: 0, g: 0, b: 0, type: 'hex'}],
['blanchedalmond', {r: 255, g: 235, b: 205, type: 'hex'}],
['blue', {r: 0, g: 0, b: 255, type: 'hex'}],
['blueviolet', {r: 138, g: 43, b: 226, type: 'hex'}],
['brown', {r: 165, g: 42, b: 42, type: 'hex'}],
['burlywood', {r: 222, g: 184, b: 135, type: 'hex'}],
['cadetblue', {r: 95, g: 158, b: 160, type: 'hex'}],
['chartreuse', {r: 127, g: 255, b: 0, type: 'hex'}],
['chocolate', {r: 210, g: 105, b: 30, type: 'hex'}],
['coral', {r: 255, g: 127, b: 80, type: 'hex'}],
['cornflowerblue', {r: 100, g: 149, b: 237, type: 'hex'}],
['cornsilk', {r: 255, g: 248, b: 220, type: 'hex'}],
['crimson', {r: 220, g: 20, b: 60, type: 'hex'}],
['cyan', {r: 0, g: 255, b: 255, type: 'hex'}],
['darkblue', {r: 0, g: 0, b: 139, type: 'hex'}],
['darkcyan', {r: 0, g: 139, b: 139, type: 'hex'}],
['darkgoldenrod', {r: 184, g: 134, b: 11, type: 'hex'}],
['darkgray', {r: 169, g: 169, b: 169, type: 'hex'}],
['darkgrey', {r: 169, g: 169, b: 169, type: 'hex'}],
['darkgreen', {r: 0, g: 100, b: 0, type: 'hex'}],
['darkkhaki', {r: 189, g: 183, b: 107, type: 'hex'}],
['darkmagenta', {r: 139, g: 0, b: 139, type: 'hex'}],
['darkolivegreen', {r: 85, g: 107, b: 47, type: 'hex'}],
['darkorange', {r: 255, g: 140, b: 0, type: 'hex'}],
['darkorchid', {r: 153, g: 50, b: 204, type: 'hex'}],
['darkred', {r: 139, g: 0, b: 0, type: 'hex'}],
['darksalmon', {r: 233, g: 150, b: 122, type: 'hex'}],
['darkseagreen', {r: 143, g: 188, b: 143, type: 'hex'}],
['darkslateblue', {r: 72, g: 61, b: 139, type: 'hex'}],
['darkslategray', {r: 47, g: 79, b: 79, type: 'hex'}],
['darkslategrey', {r: 47, g: 79, b: 79, type: 'hex'}],
['darkturquoise', {r: 0, g: 206, b: 209, type: 'hex'}],
['darkviolet', {r: 148, g: 0, b: 211, type: 'hex'}],
['deeppink', {r: 255, g: 20, b: 147, type: 'hex'}],
['deepskyblue', {r: 0, g: 191, b: 255, type: 'hex'}],
['dimgray', {r: 105, g: 105, b: 105, type: 'hex'}],
['dimgrey', {r: 105, g: 105, b: 105, type: 'hex'}],
['dodgerblue', {r: 30, g: 144, b: 255, type: 'hex'}],
['firebrick', {r: 178, g: 34, b: 34, type: 'hex'}],
['floralwhite', {r: 255, g: 250, b: 240, type: 'hex'}],
['forestgreen', {r: 34, g: 139, b: 34, type: 'hex'}],
['fuchsia', {r: 255, g: 0, b: 255, type: 'hex'}],
['gainsboro', {r: 220, g: 220, b: 220, type: 'hex'}],
['ghostwhite', {r: 248, g: 248, b: 255, type: 'hex'}],
['gold', {r: 255, g: 215, b: 0, type: 'hex'}],
['goldenrod', {r: 218, g: 165, b: 32, type: 'hex'}],
['gray', {r: 128, g: 128, b: 128, type: 'hex'}],
['grey', {r: 128, g: 128, b: 128, type: 'hex'}],
['green', {r: 0, g: 128, b: 0, type: 'hex'}],
['greenyellow', {r: 173, g: 255, b: 47, type: 'hex'}],
['honeydew', {r: 240, g: 255, b: 240, type: 'hex'}],
['hotpink', {r: 255, g: 105, b: 180, type: 'hex'}],
['indianred', {r: 205, g: 92, b: 92, type: 'hex'}],
['indigo', {r: 75, g: 0, b: 130, type: 'hex'}],
['ivory', {r: 255, g: 255, b: 240, type: 'hex'}],
['khaki', {r: 240, g: 230, b: 140, type: 'hex'}],
['lavender', {r: 230, g: 230, b: 250, type: 'hex'}],
['lavenderblush', {r: 255, g: 240, b: 245, type: 'hex'}],
['lawngreen', {r: 124, g: 252, b: 0, type: 'hex'}],
['lemonchiffon', {r: 255, g: 250, b: 205, type: 'hex'}],
['lightblue', {r: 173, g: 216, b: 230, type: 'hex'}],
['lightcoral', {r: 240, g: 128, b: 128, type: 'hex'}],
['lightcyan', {r: 224, g: 255, b: 255, type: 'hex'}],
['lightgoldenrodyellow', {r: 250, g: 250, b: 210, type: 'hex'}],
['lightgray', {r: 211, g: 211, b: 211, type: 'hex'}],
['lightgrey', {r: 211, g: 211, b: 211, type: 'hex'}],
['lightgreen', {r: 144, g: 238, b: 144, type: 'hex'}],
['lightpink', {r: 255, g: 182, b: 193, type: 'hex'}],
['lightsalmon', {r: 255, g: 160, b: 122, type: 'hex'}],
['lightseagreen', {r: 32, g: 178, b: 170, type: 'hex'}],
['lightskyblue', {r: 135, g: 206, b: 250, type: 'hex'}],
['lightslategray', {r: 119, g: 136, b: 153, type: 'hex'}],
['lightslategrey', {r: 119, g: 136, b: 153, type: 'hex'}],
['lightsteelblue', {r: 176, g: 196, b: 222, type: 'hex'}],
['lightyellow', {r: 255, g: 255, b: 224, type: 'hex'}],
['lime', {r: 0, g: 255, b: 0, type: 'hex'}],
['limegreen', {r: 50, g: 205, b: 50, type: 'hex'}],
['linen', {r: 250, g: 240, b: 230, type: 'hex'}],
['magenta', {r: 255, g: 0, b: 255, type: 'hex'}],
['maroon', {r: 128, g: 0, b: 0, type: 'hex'}],
['mediumaquamarine', {r: 102, g: 205, b: 170, type: 'hex'}],
['mediumblue', {r: 0, g: 0, b: 205, type: 'hex'}],
['mediumorchid', {r: 186, g: 85, b: 211, type: 'hex'}],
['mediumpurple', {r: 147, g: 112, b: 219, type: 'hex'}],
['mediumseagreen', {r: 60, g: 179, b: 113, type: 'hex'}],
['mediumslateblue', {r: 123, g: 104, b: 238, type: 'hex'}],
['mediumspringgreen', {r: 0, g: 250, b: 154, type: 'hex'}],
['mediumturquoise', {r: 72, g: 209, b: 204, type: 'hex'}],
['mediumvioletred', {r: 199, g: 21, b: 133, type: 'hex'}],
['midnightblue', {r: 25, g: 25, b: 112, type: 'hex'}],
['mintcream', {r: 245, g: 255, b: 250, type: 'hex'}],
['mistyrose', {r: 255, g: 228, b: 225, type: 'hex'}],
['moccasin', {r: 255, g: 228, b: 181, type: 'hex'}],
['navajowhite', {r: 255, g: 222, b: 173, type: 'hex'}],
['navy', {r: 0, g: 0, b: 128, type: 'hex'}],
['oldlace', {r: 253, g: 245, b: 230, type: 'hex'}],
['olive', {r: 128, g: 128, b: 0, type: 'hex'}],
['olivedrab', {r: 107, g: 142, b: 35, type: 'hex'}],
['orange', {r: 255, g: 165, b: 0, type: 'hex'}],
['orangered', {r: 255, g: 69, b: 0, type: 'hex'}],
['orchid', {r: 218, g: 112, b: 214, type: 'hex'}],
['palegoldenrod', {r: 238, g: 232, b: 170, type: 'hex'}],
['palegreen', {r: 152, g: 251, b: 152, type: 'hex'}],
['paleturquoise', {r: 175, g: 238, b: 238, type: 'hex'}],
['palevioletred', {r: 219, g: 112, b: 147, type: 'hex'}],
['papayawhip', {r: 255, g: 239, b: 213, type: 'hex'}],
['peachpuff', {r: 255, g: 218, b: 185, type: 'hex'}],
['peru', {r: 205, g: 133, b: 63, type: 'hex'}],
['pink', {r: 255, g: 192, b: 203, type: 'hex'}],
['plum', {r: 221, g: 160, b: 221, type: 'hex'}],
['powderblue', {r: 176, g: 224, b: 230, type: 'hex'}],
['purple', {r: 128, g: 0, b: 128, type: 'hex'}],
['rebeccapurple', {r: 102, g: 51, b: 153, type: 'hex'}],
['red', {r: 255, g: 0, b: 0, type: 'hex'}],
['rosybrown', {r: 188, g: 143, b: 143, type: 'hex'}],
['royalblue', {r: 65, g: 105, b: 225, type: 'hex'}],
['saddlebrown', {r: 139, g: 69, b: 19, type: 'hex'}],
['salmon', {r: 250, g: 128, b: 114, type: 'hex'}],
['sandybrown', {r: 244, g: 164, b: 96, type: 'hex'}],
['seagreen', {r: 46, g: 139, b: 87, type: 'hex'}],
['seashell', {r: 255, g: 245, b: 238, type: 'hex'}],
['sienna', {r: 160, g: 82, b: 45, type: 'hex'}],
['silver', {r: 192, g: 192, b: 192, type: 'hex'}],
['skyblue', {r: 135, g: 206, b: 235, type: 'hex'}],
['slateblue', {r: 106, g: 90, b: 205, type: 'hex'}],
['slategray', {r: 112, g: 128, b: 144, type: 'hex'}],
['slategrey', {r: 112, g: 128, b: 144, type: 'hex'}],
['snow', {r: 255, g: 250, b: 250, type: 'hex'}],
['springgreen', {r: 0, g: 255, b: 127, type: 'hex'}],
['steelblue', {r: 70, g: 130, b: 180, type: 'hex'}],
['tan', {r: 210, g: 180, b: 140, type: 'hex'}],
['teal', {r: 0, g: 128, b: 128, type: 'hex'}],
['thistle', {r: 216, g: 191, b: 216, type: 'hex'}],
['tomato', {r: 255, g: 99, b: 71, type: 'hex'}],
['turquoise', {r: 64, g: 224, b: 208, type: 'hex'}],
['violet', {r: 238, g: 130, b: 238, type: 'hex'}],
['wheat', {r: 245, g: 222, b: 179, type: 'hex'}],
['white', {r: 255, g: 255, b: 255, type: 'hex'}],
['whitesmoke', {r: 245, g: 245, b: 245, type: 'hex'}],
['yellow', {r: 255, g: 255, b: 0, type: 'hex'}],
['yellowgreen', {r: 154, g: 205, b: 50, type: 'hex'}],
]));

View File

@ -301,7 +301,7 @@ self.parserlib = (() => {
'font-variant-position': 'normal | sub | super',
'font-variation-settings': 'normal | [ <string> <number> ]#',
'font-weight': '<font-weight>',
'forced-color-adjust': 'auto | none',
'forced-color-adjust': 'auto | none | preserve-parent-color',
'-ms-flex-align': 1,
'-ms-flex-order': 1,
'-ms-flex-pack': 1,
@ -3503,7 +3503,7 @@ self.parserlib = (() => {
do {
this._ws();
if ((t = stream.get(true)).type === Tokens.IDENT) {
ids.push(t.value);
ids.push(this._layerName(t));
this._ws();
t = stream.get(true);
}
@ -3521,6 +3521,16 @@ self.parserlib = (() => {
this._ws();
}
_layerName(start) {
let res = '';
const stream = this._tokenStream;
for (let t; (t = start || stream.match(Tokens.IDENT));) {
res += t.value + (stream.match(Tokens.DOT) ? '.' : '');
start = false;
}
return res;
}
_stylesheet() {
const stream = this._tokenStream;
this.fire('startstylesheet');
@ -3590,7 +3600,7 @@ self.parserlib = (() => {
this._ws();
t = stream.get(true);
if (/^layer(\()?$/i.test(t.value)) {
layer = RegExp.$1 ? stream.mustMatch(Tokens.IDENT) : '';
layer = RegExp.$1 ? this._layerName() : '';
if (layer) stream.mustMatch(Tokens.RPAREN);
this._ws();
t = stream.get(true);

View File

@ -13,6 +13,7 @@
messageBoxProxy
moveFocus
scrollElementIntoView
setInputValue
setupLivePrefs
showSpinner
toggleDataset
@ -296,6 +297,18 @@ function scrollElementIntoView(element, {invalidMarginRatio = 0} = {}) {
}
}
function setInputValue(input, value) {
input.focus();
input.select();
// using execCommand to add to the input's undo history
document.execCommand(value ? 'insertText' : 'delete', false, value);
// some versions of Firefox ignore execCommand
if (input.value !== value) {
input.value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
}
}
/**
* 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
@ -314,9 +327,7 @@ function setupLivePrefs(ids) {
function getValue(el) {
const type = el.dataset.valueType || el.type;
return type === 'checkbox' ? el.checked :
// https://stackoverflow.com/questions/18062069/why-does-valueasnumber-return-nan-as-a-value
// valueAsNumber is not applicable for input[text/radio] or select
type === 'number' ? Number(el.value) :
type === 'number' ? parseFloat(el.value) :
el.value;
}
function isSame(el, oldValue, value) {
@ -465,6 +476,7 @@ prefs.ready.then(() => {
const max = (innerWidth < 850 ? screen.availWidth : innerWidth) / 3;
width = Math.round(Math.max(200, Math.min(max, Number(width) || 0)));
$.root.style.setProperty('--header-width', width + 'px');
dom.HWval = width;
return width;
},
});
@ -481,9 +493,7 @@ prefs.ready.then(() => {
}));
}
}
window.requestIdleCallback(() => {
require(lazyScripts);
});
setTimeout(() => requestIdleCallback(() => require(lazyScripts)));
})();
//#endregion

View File

@ -11,13 +11,18 @@
*/
function t(key, params, strict = true) {
const s = chrome.i18n.getMessage(key, params);
const s = !params && t.cache[key]
|| (t.cache[key] = chrome.i18n.getMessage(key, params));
if (!s && strict) throw `Missing string "${key}"`;
return s;
}
Object.assign(t, {
template: {},
cache: {},
template: new Proxy({}, {
get: (obj, k, _) => obj[k] ||
(_ = $(`template[data-id="${k}"]`)) && (obj[k] = t.createTemplate(_)),
}),
ALLOWED_TAGS: ['a', 'b', 'code', 'i', 'sub', 'sup', 'wbr'],
RX_WORD_BREAK: new RegExp([
'(',
@ -27,9 +32,9 @@ Object.assign(t, {
'|',
/((?!\s)\W){10}/,
')',
/(?!\b|\s|$)/,
/(?!\s|$)/,
].map(rx => rx.source || rx).join(''), 'gu'),
SELECTOR: '[i18n], template',
SELECTOR: '[i18n]',
HTML(html) {
return typeof html !== 'string'
@ -45,10 +50,6 @@ Object.assign(t, {
}
for (const node of nodes) {
if (!node.localName) continue;
if (node.localName === 'template') {
t.createTemplate(node);
continue;
}
const attr = node.getAttribute('i18n');
if (!attr) continue;
for (const part of attr.split(',')) {

View File

@ -55,6 +55,7 @@
'manage.actions.expanded': true,
'manage.backup.expanded': true,
'manage.filters.expanded': true,
'manage.minColumnWidth': 750,
// the new compact layout doesn't look good on Android yet
'manage.newUI': true,
'manage.newUI.favicons': false, // show favicons for the sites in applies-to
@ -98,7 +99,7 @@
// "Delete" item in context menu for browsers that don't have it
'editor.contextDelete': false,
'editor.selectByTokens': true,
'editor.arrowKeysTraverse': true,
'editor.appliesToLineWidget': true, // show applies-to line widget on the editor
'editor.autosaveDraft': 10, // seconds
'editor.livePreview': true,

View File

@ -78,9 +78,16 @@ const router = {
router.update();
},
updateSearch(key, value) {
/**
* @param {Object|string} what - an object or a single key
* @param {string} [value] - for `key` mode
*/
updateSearch(what, value) {
const u = new URL(location);
u.searchParams[value ? 'set' : 'delete'](key, value);
const entries = typeof what === 'object' ? Object.entries(what) : [[what, value]];
for (const [key, val] of entries) {
u.searchParams[val ? 'set' : 'delete'](key, val);
}
history.replaceState(history.state, null, `${u}`);
router.update(true);
},

View File

@ -5,7 +5,6 @@
MozDocMapper
styleCodeEmpty
styleJSONseemsValid
styleSectionGlobal
styleSectionsEqual
*/
@ -83,14 +82,6 @@ function styleCodeEmpty(code) {
return false;
}
/** Checks if section is global i.e. has no targets at all */
function styleSectionGlobal(section) {
return (!section.regexps || !section.regexps.length) &&
(!section.urlPrefixes || !section.urlPrefixes.length) &&
(!section.urls || !section.urls.length) &&
(!section.domains || !section.domains.length);
}
/**
* The sections are checked in successive order because it matters when many sections
* match the same URL and they have rules with the same CSS specificity

View File

@ -160,40 +160,6 @@ async function getActiveTab() {
return (await browser.tabs.query({currentWindow: true, active: true}))[0];
}
function urlToMatchPattern(url, ignoreSearch) {
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Match_patterns
if (!/^(http|https|ws|wss|ftp|data|file)$/.test(url.protocol)) {
return undefined;
}
if (ignoreSearch) {
return [
`${url.protocol}//${url.hostname}/${url.pathname}`,
`${url.protocol}//${url.hostname}/${url.pathname}?*`,
];
}
// FIXME: is %2f allowed in pathname and search?
return `${url.protocol}//${url.hostname}/${url.pathname}${url.search}`;
}
async function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSearch = false}) {
url = tryURL(url);
const tabs = await browser.tabs.query({
url: urlToMatchPattern(url, ignoreSearch),
currentWindow,
});
return tabs.find(tab => {
const tabUrl = tryURL(tab.pendingUrl || tab.url);
return tabUrl.protocol === url.protocol &&
tabUrl.username === url.username &&
tabUrl.password === url.password &&
tabUrl.hostname === url.hostname &&
tabUrl.port === url.port &&
tabUrl.pathname === url.pathname &&
(ignoreSearch || tabUrl.search === url.search) &&
(ignoreHash || tabUrl.hash === url.hash);
});
}
/**
* Opens a tab or activates an existing one,
* reuses the New Tab page or about:blank if it's focused now
@ -204,7 +170,7 @@ async function findExistingTab({url, currentWindow, ignoreHash = true, ignoreSea
* @param {Boolean} [_.active=true] `true` to activate the tab
* @param {Boolean|null} [_.currentWindow=true] `null` to check all windows
* @param {chrome.windows.CreateData} [_.newWindow] creates a new window with these params if specified
* @param {boolean} [_.ignoreExisting] specify to skip findExistingTab
* @param {boolean} [_.newTab] `true` to force a new tab instead of switching to an existing tab
* @returns {Promise<chrome.tabs.Tab>} Promise -> opened/activated tab
*/
async function openURL({
@ -214,12 +180,12 @@ async function openURL({
active = true,
currentWindow = true,
newWindow,
ignoreExisting,
newTab,
}) {
if (!url.includes('://')) {
url = chrome.runtime.getURL(url);
}
let tab = !ignoreExisting && await findExistingTab({url, currentWindow});
let tab = !newTab && (await browser.tabs.query({url: url.split('#')[0], currentWindow}))[0];
if (tab) {
return activateTab(tab, {
index,

View File

@ -18,6 +18,8 @@
<a target="_blank" class="homepage"></a>
</h2>
<p class="applies-to">
<span class="style-info" data-type="age"></span>.
<label i18n="genericSize">:</label><span class="style-info" data-type="size"></span>.
<label i18n="appliesDisplay"></label>
<span class="targets"></span>
</p>
@ -29,7 +31,7 @@
<button class="disable" i18n="disableStyleLabel"></button>
<button class="delete" i18n="deleteStyleLabel"></button>
<button class="check-update" i18n="checkForUpdate"></button>
<button class="update" i18n="installUpdate"></button>
<button class="update" i18n="installUpdate" hidden></button>
<button class="configure-usercss" i18n="configureStyle"></button>
<span class="update-note"></span>
</p>
@ -47,9 +49,9 @@
&nbsp;
<span class="style-info" data-type="version"></span>
</a>
<a target="_blank" class="homepage" tabindex="0"></a>
</h2>
<p class="actions">
<a target="_blank" class="homepage" tabindex="0"></a>
<a class="delete" i18n="title:deleteStyleLabel" tabindex="0">
<svg class="svg-icon" 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
@ -57,6 +59,7 @@
</svg>
</a>
</p>
<p class="style-info" data-type="size"></p>
<p class="style-info" data-type="age"></p>
<div class="applies-to">
<a class="expander" tabindex="0">
@ -388,6 +391,7 @@
</template>
<link rel="stylesheet" href="manage/manage.css">
<link rel="stylesheet" href="manage/manage-newui.css" id="newUI">
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
</head>

View File

@ -1,7 +1,7 @@
/* global API */// msg.js
/* global changeQueue installed newUI */// manage.js
/* global checkUpdate handleUpdateInstalled */// updater-ui.js
/* global createStyleElement createTargetsElement getFaviconSrc */// render.js
/* global createStyleElement createTargetsElement getFaviconSrc styleToDummyEntry */// render.js
/* global debounce getOwnTab openURL sessionStore */// toolbox.js
/* global filterAndAppend showFiltersStats */// filters.js
/* global sorter */
@ -130,7 +130,9 @@ const Events = {
},
name(event, entry) {
if (newUI.enabled) Events.edit(event, entry);
if (newUI.enabled && !event.target.closest('.homepage')) {
Events.edit(event, entry);
}
},
toggle(event, entry) {
@ -191,9 +193,9 @@ function handleUpdate(style, {reason, method} = {}) {
if (oldEntry && method === 'styleUpdated') {
handleToggledOrCodeOnly();
}
entry = entry || createStyleElement({style});
entry = entry || createStyleElement(styleToDummyEntry(style));
if (oldEntry) {
if (oldEntry.styleNameLowerCase === entry.styleNameLowerCase) {
if (oldEntry.styleNameLC === entry.styleNameLC) {
installed.replaceChild(entry, oldEntry);
} else {
oldEntry.remove();

View File

@ -114,7 +114,7 @@ function initFilters() {
}
}
filterOnChange({forceRefilter: true});
router.updateSearch('search', '');
router.updateSearch({search: '', searchMode: ''});
};
filterOnChange({forceRefilter: true});

View File

@ -57,7 +57,7 @@
let found;
for (const entry of rotated || entries) {
if (entry.classList.contains('hidden')) continue;
const name = entry.styleNameLowerCase;
const name = entry.styleNameLC;
const pos = name.indexOf(text);
if (pos === 0) {
found = entry;
@ -73,7 +73,7 @@
if (found && found !== focusedEntry) {
focusedEntry = found;
focusedLink = $('a', found);
focusedName = found.styleNameLowerCase;
focusedName = found.styleNameLC;
scrollElementIntoView(found, {invalidMarginRatio: .25});
animateElement(found, 'highlight-quick');
replaceInlineStyle({

281
manage/manage-newui.css Normal file
View File

@ -0,0 +1,281 @@
.disabled.entry .svg-icon {
color: var(--c50);
fill: var(--c80);
font-weight: normal;
transition: color .5s .1s, fill .5s .1s;
}
#installed {
margin-top: .75rem;
margin-bottom: .75rem;
}
.entry {
padding: 0 .5em;
display: flex;
flex-wrap: wrap;
border: none;
}
.entry.odd {
background-color: rgba(128, 128, 128, 0.05);
}
.entry > * {
padding: .5rem 0;
margin: 0;
display: flex;
align-items: center;
}
.entry .actions {
position: relative;
}
.style-info[data-type=size],
.style-info[data-type=age] {
color: var(--c50);
justify-content: end;
}
.style-info[data-type=age] {
flex: 0 0 4ch;
}
.style-info[data-type=size] {
flex: 0 0 var(--size-width);
}
.style-info[data-type=version] {
color: var(--c40);
padding-left: .5em;
font-weight: normal;
}
.style-info[data-type=version][data-is-date],
.style-info[data-type=version][data-value=""],
.style-info[data-type=version][data-value="1.0.0"] {
display: none;
}
.entry input[type="checkbox"]:not(.slider) {
pointer-events: all;
}
.style-name {
font-size: 14px;
padding-left: var(--name-padding-left);
position: relative;
cursor: pointer;
justify-content: space-between;
flex: 0 0 var(--name-width);
min-width: 25%;
max-width: 50%;
}
#installed[style*="--num-targets:0"] .style-name {
max-width: none;
flex-grow: 1;
}
.entry .checkmate {
flex-shrink: 0;
}
.style-name-link {
width: 100%;
word-break: break-word;
}
.style-name:hover .style-name-link {
text-decoration: underline;
}
.entry.enabled .style-name:hover .style-name-link {
color: var(--accent-1);
}
.homepage {
margin-top: -3px;
}
.actions {
flex: 0 0 calc(3 * (var(--action-size) + var(--action-margin)));
flex-wrap: nowrap;
z-index: 100;
}
.actions > * {
width: var(--action-size);
height: var(--action-size);
display: flex;
align-items: center;
}
.updater-icons > * {
transition: opacity 1s;
display: none;
}
.entry .svg-icon {
fill: var(--c60);
}
.entry:hover .svg-icon {
fill: var(--c40);
}
.entry .svg-icon.checked,
.entry:hover .svg-icon.checked,
.entry:hover .svg-icon:hover {
fill: var(--fg);
}
.checking-update .check-update {
opacity: 0;
display: inline-block;
pointer-events: none;
}
.can-update .update,
.no-update:not(.update-problem):not(.update-done) .up-to-date,
.no-update.update-problem .check-update,
.update-done .updated {
display: inline-block;
}
.up-to-date svg,
.updated svg {
cursor: auto;
}
.update-done .updated svg {
top: -4px;
position: relative;
filter: drop-shadow(0 5px 0 currentColor);
}
.can-update .update,
.no-update.update-problem .check-update {
cursor: pointer;
}
.can-update[data-details$="locally edited"] .update svg,
.update-problem .check-update svg {
fill: #ef6969;
}
.can-update[data-details$="locally edited"]:hover .update svg,
.entry.update-problem:hover .check-update svg {
fill: #fd4040;
}
.can-update[data-details$="locally edited"]:hover .update svg:hover,
.entry.update-problem:hover .check-update svg:hover {
fill: red;
}
.updater-icons > :not(.check-update):after {
content: attr(title);
position: absolute;
display: block;
width: max-content;
max-width: 25vw;
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;
}
.update-problem .check-update:after {
background-color: red;
border: 1px solid #d40000;
color: white;
animation: none;
}
.can-update .update:after {
animation: none;
}
.can-update:not([data-details$="locally edited"]) .update:after {
background-color: #c0fff0;
border: 1px solid #89cac9;
}
.applies-to {
padding: .25em 0 .25em 1em;
flex-grow: 999;
}
#installed[style*="--num-targets:0"] .applies-to {
display: none;
}
.targets {
overflow: hidden;
max-height: calc(var(--num-targets) * 18px);
width: 100%;
}
.expander {
cursor: pointer;
position: absolute;
top: 0;
right: 4px;
bottom: 0;
display: flex;
align-items: center;
}
.expander:hover {
background: hsla(0, 0%, 50%, .05);
}
.expander svg {
width: 16px;
height: 16px;
fill: var(--c50);
}
.applies-to:not(.has-more) .expander {
display: none;
}
.target {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-right: 1em;
line-height: 18px;
width: 0;
min-width: 100%;
box-sizing: border-box;
}
.target img {
width: 16px;
height: 16px;
vertical-align: middle;
margin: -1px 4px 0 -20px;
transition: opacity .5s, filter .5s;
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden;
visibility: hidden;
}
.expanded svg {
transform: rotate(180deg);
transform-origin: 8px 8px;
}
.applies-to.expanded {
max-width: 100%;
align-items: flex-start;
}
.applies-to.expanded .targets {
max-height: none;
display: flex;
flex-wrap: wrap;
}
.applies-to.expanded .target {
width: auto;
min-width: 18em;
overflow-wrap: anywhere;
white-space: normal;
}
.favicons-grayed .target img {
filter: grayscale(1);
opacity: .25;
}
.has-favicons .target {
padding-left: 20px;
}
.has-favicons .target img[src] {
visibility: visible;
}
.entry:hover .target img {
opacity: 1;
filter: none;
}
.target b::after {
content: '?';
margin: -2px 4px 0 -20px;
display: inline-block;
vertical-align: baseline;
background: var(--c85);
width: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
color: var(--bg);
}
@media (max-width: 850px) {
:root {
--name-padding-left: 14px;
}
.entry {
padding: 0;
}
.entry > .style-info {
display: none;
}
}

View File

@ -1,7 +1,9 @@
:root {
--name-padding-left: 20px;
--name-padding-right: 40px;
--actions-width: 75px;
--name-width: 30ch;
--size-width: 4ch;
--action-size: 20px;
--action-margin: 6px;
}
body {
/* Fill the entire viewport to enable json import via drag'n'drop */
@ -124,17 +126,21 @@ a:hover {
}
#installed {
position: relative;
padding-left: var(--header-width);
box-sizing: border-box;
width: 100%;
align-self: start;
display: flex;
flex-wrap: wrap;
}
.entry {
margin: 0;
padding: 1.25em 2em;
border-top: 1px solid var(--c85);
box-sizing: border-box;
position: relative;
width: calc(100% / var(--columns, 1));
}
.entry:first-child {
@ -158,15 +164,13 @@ a:hover {
margin-top: .25em;
overflow-wrap: break-word;
}
.style-name a, .style-edit-link {
.entry a:not(:hover) {
text-decoration: none;
}
.style-name span,
.applies-to {
overflow-wrap: break-word;
overflow-wrap: anywhere;
}
.applies-to,
.actions {
padding-left: 15px;
@ -181,18 +185,17 @@ a:hover {
.actions > * {
margin-bottom: .25rem;
display: inline-block;
}
.actions > *:not(:last-child) {
margin-right: .25rem;
margin-right: var(--action-margin);
}
.applies-to label {
margin-right: .5ex;
}
.applies-to .target:hover {
.oldUI .applies-to .target:hover {
background-color: rgba(128, 128, 128, .15);
}
@ -240,8 +243,7 @@ a:hover {
}
.disabled h2 .style-name-link,
.disabled .applies-to,
.newUI .disabled.entry .svg-icon {
.disabled .applies-to {
color: var(--c50);
fill: var(--c80);
font-weight: normal;
@ -319,55 +321,8 @@ a:hover {
/* compact layout */
.newUI #installed {
display: table;
margin-top: .75rem;
margin-bottom: .75rem;
}
.newUI .entry {
display: table-row;
padding-top: 0;
padding-bottom: 0;
}
.newUI .entry.odd {
background-color: rgba(128, 128, 128, 0.05);
}
.newUI .entry > * {
padding: .5rem 0;
margin: 0;
display: table-cell;
vertical-align: middle;
}
.newUI .entry .actions {
position: relative;
}
.style-info[data-type=version] {
color: var(--c40);
padding-left: .5em;
font-weight: normal;
}
.newUI .style-info[data-type=version][data-is-date],
.newUI .style-info[data-type=version][data-value=""],
.newUI .style-info[data-type=version][data-value="1.0.0"] {
display: none;
}
.newUI .entry .style-info[data-type=age] {
color: var(--c60);
text-align: right;
padding-right: 1em;
}
/************ checkbox & select************/
#newUIoptions > div, #newUIoptions > label {
margin: 4px 0;
}
.filter-selection {
position: relative;
left: -9px;
@ -392,10 +347,6 @@ a:hover {
margin-top: -2px;
}
.newUI #newUIoptions > label {
padding-left: 0;
}
.filter-selection select {
height: 18px;
border: none;
@ -438,7 +389,7 @@ a:hover {
.entry .checkmate {
vertical-align: middle;
margin: -2px 1ex 0 0;
margin-right: 1ch;
}
#manage-text {
@ -455,242 +406,10 @@ a:hover {
margin: 0 .5em;
}
.newUI .entry input[type="checkbox"]:not(.slider) {
pointer-events: all;
}
.newUI .style-name {
font-size: 14px;
padding-left: var(--name-padding-left);
padding-right: var(--name-padding-right);
position: relative;
cursor: pointer;
}
.newUI .entry .style-name:hover::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to right, hsla(180, 50%, 30%, 0.2), hsla(180, 20%, 10%, 0.05) 50%, transparent);
pointer-events: none;
}
.newUI .entry.enabled .style-name:hover .style-name-link {
color: var(--accent-1);
}
.newUI .style-name:after {
text-indent: 1.2rem;
}
.newUI .actions:after {
text-indent: -25px;
}
.newUI .actions .homepage[href=""] {
display: inline-block;
visibility: hidden;
height: 0;
}
.newUI .actions {
width: var(--actions-width);
height: 20px;
white-space: nowrap;
}
.newUI .actions > * {
margin: 0 6px 0 0;
width: 20px;
height: 20px;
}
.newUI .updater-icons > * {
transition: opacity 1s;
display: none;
}
.newUI .entry .svg-icon {
fill: var(--c60);
}
.newUI .entry:hover .svg-icon {
fill: var(--c40);
}
button .svg-icon,
.newUI .entry .svg-icon.checked,
.newUI .entry:hover .svg-icon.checked,
.newUI .entry:hover .svg-icon:hover {
button .svg-icon {
fill: var(--fg);
}
.newUI .checking-update .check-update {
opacity: 0;
display: inline-block;
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-block;
}
.newUI .up-to-date svg,
.newUI .updated svg {
cursor: auto;
}
.newUI .update-done .updated svg {
top: -4px;
position: relative;
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 .actions {
z-index: 100;
}
.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;
}
.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;
max-height: calc(var(--num-targets) * 18px);
}
.newUI .applies-to.expanded .targets {
max-height: none;
}
.newUI .target {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(75vw - var(--header-width) - var(--actions-width) - var(--name-padding-left) - var(--name-padding-right) - 6rem);
box-sizing: border-box;
padding-right: 1rem;
line-height: 18px;
}
.expander {
cursor: pointer;
position: absolute;
right: 4px;
}
.expander svg {
width: 16px;
height: 16px;
fill: var(--c50);
}
.expanded .expander {
transform: rotate(180deg);
transform-origin: 8px 8px;
}
.newUI .applies-to:not(.has-more) .expander {
display: none;
}
.newUI .target:hover {
background-color: inherit;
}
.newUI .target img {
width: 16px;
height: 16px;
vertical-align: middle;
margin: -1px 4px 0 -20px;
transition: opacity .5s, filter .5s;
/* workaround for the buggy CSS filter: images in the hidden overflow are shown on Mac */
backface-visibility: hidden;
visibility: hidden;
}
.newUI .favicons-grayed .target img {
filter: grayscale(1);
opacity: .25;
}
.newUI .has-favicons .target {
padding-left: 20px;
}
.newUI .has-favicons .target img[src] {
visibility: visible;
}
.newUI .entry:hover .target img {
opacity: 1;
filter: none;
}
.newUI .target b::after {
content: '?';
margin: -2px 4px 0 -20px;
display: inline-block;
vertical-align: baseline;
background: var(--c85);
width: 16px;
line-height: 16px;
text-align: center;
border-radius: 50%;
color: var(--bg);
}
/* Default, no update buttons */
.updater-icons .update,
.updater-icons .check-update {
@ -765,7 +484,7 @@ button .svg-icon,
/* highlight updated/added styles */
.highlight {
animation: highlight 10s cubic-bezier(0,.82,.47,.98);
animation: highlight 1s cubic-bezier(0, .4, .6, 1);
}
.highlight-quick {
animation: highlight .5s;
@ -1031,19 +750,10 @@ button .svg-icon,
}
}
@media (max-width: 1000px) {
.newUI .entry > .style-info {
display: none;
}
}
@media (max-width: 850px) {
:root {
--name-padding-left: 34px;
}
body {
display: table;
display: block;
height: auto;
}
body.all-styles-hidden-by-filters:before {
@ -1057,18 +767,10 @@ button .svg-icon,
left: 3.75rem;
}
html:not(.newUI) .applies-to {
.oldUI .applies-to {
word-break: break-all;
}
#installed {
table-layout: fixed;
}
.newUI .entry .actions {
padding-right: 30px
}
#search-wrapper,
#sort-wrapper,
#header summary {
@ -1084,11 +786,13 @@ button .svg-icon,
}
#header {
display: table-header-group;
height: auto;
padding: 0;
width: 100%;
position: static;
position: sticky;
top: 0;
z-index: 1000;
background: var(--bg);
border-right: none;
border-bottom: 1px dashed var(--c65);
}
@ -1113,54 +817,14 @@ button .svg-icon,
padding-left: 0;
}
#header h1,
#backup-message {
#header h1 {
display: none;
}
#backup-buttons {
margin-top: 0;
}
#header summary {
margin-top: 0;
padding-bottom: .25rem;
}
.newUI .entry {
padding: 0;
}
.newUI .entry .checkmate {
position: absolute;
left: 14px;
top: 0;
bottom: 0;
margin: auto;
}
.newUI .entry .style-name {
text-indent: unset;
}
.newUI .entry .actions {
width: 104px;
padding: .5rem 0 .5rem 6px;
}
.newUI .entry .applies-to {
padding: .25rem .5rem .25rem 0;
}
.newUI .entry .target {
max-width: 100%;
padding-right: 0;
}
.newUI .style-name-link::after {
text-indent: 0;
display: inline-block;
}
}
@supports (-moz-appearance: none) {

View File

@ -28,9 +28,16 @@ const newUI = {
Object.assign(newUI, {
ids: Object.keys(newUI),
prefKeyForId: id => `manage.newUI.${id}`.replace(/\.enabled$/, ''),
readPrefs(dest = newUI, cb) {
for (const id of newUI.ids) {
const val = dest[id] = prefs.get(newUI.prefKeyForId(id));
if (cb) cb(id, val);
}
},
renderClass: () => {
$.rootCL.toggle('newUI', newUI.enabled);
$.rootCL.toggle('oldUI', !newUI.enabled);
$('#newUI').media = newUI.enabled ? '' : '?';
},
hasFavs: () => newUI.enabled && newUI.favicons,
badFavsKey: 'badFavs',
@ -41,9 +48,7 @@ Object.assign(newUI, {
},
});
// ...read the actual values
for (const id of newUI.ids) {
newUI[id] = prefs.get(newUI.prefKeyForId(id));
}
newUI.readPrefs();
newUI.renderClass();
(async function init() {

View File

@ -3,7 +3,6 @@
/* global URLS debounce getOwnTab isEmptyObj sessionStore stringAsRegExp */// toolbox.js
/* global filterAndAppend */// filters.js
/* global installed newUI */// manage.js
/* global prefs */
/* global sorter */
/* global t */// localization.js
'use strict';
@ -17,6 +16,8 @@ const AGES = [
[12, 'm', t('dateAbbrMonth', '\x01')],
[Infinity, 'y', t('dateAbbrYear', '\x01')],
];
const groupThousands = num => `${num}`.replace(/\d(?=(\d{3})+$)/g, '$&\xA0');
const renderSize = size => groupThousands(Math.round(size / 1024)) + 'k';
(() => {
const proto = HTMLImageElement.prototype;
@ -70,7 +71,15 @@ function createAgeText(el, style) {
}
}
function createStyleElement({style, name: nameLC}) {
function calcObjSize(obj) {
// Inaccurate but simple
return typeof obj !== 'object' ? `${obj}`.length :
!obj ? 0 :
Array.isArray(obj) ? obj.reduce((sum, v) => sum + calcObjSize(v), 0) :
Object.entries(obj).reduce((sum, [k, v]) => sum + k.length + calcObjSize(v), 0);
}
function createStyleElement({styleMeta: style, styleNameLC: nameLC, styleSize: size}) {
// query the sub-elements just once, then reuse the references
if ((elementParts || {}).newUI !== newUI.enabled) {
const entry = t.template[newUI.enabled ? 'styleNewUI' : 'style'].cloneNode(true);
@ -85,6 +94,7 @@ function createStyleElement({style, name: nameLC}) {
homepage: $('.homepage', entry),
homepageIcon: t.template[`homepageIcon${newUI.enabled ? 'Small' : 'Big'}`],
infoAge: $('[data-type=age]', entry),
infoSize: $('[data-type=size]', entry),
infoVer: $('[data-type=version]', entry),
appliesTo: $('.applies-to', entry),
targets: $('.targets', entry),
@ -95,7 +105,6 @@ function createStyleElement({style, name: nameLC}) {
},
oldConfigure: !newUI.enabled && $('.configure-usercss', entry),
oldCheckUpdate: !newUI.enabled && $('.check-update', entry),
oldUpdate: !newUI.enabled && $('.update', entry),
};
}
const parts = elementParts;
@ -114,12 +123,13 @@ function createStyleElement({style, name: nameLC}) {
} else {
delete parts.infoVer.dataset.isDate;
}
if (newUI.enabled) {
createAgeText(parts.infoAge, style);
} else {
createAgeText(parts.infoAge, style);
parts.infoSize.dataset.value = Math.log10(size || 1) >> 0; // for CSS to target big/small styles
parts.infoSize.textContent = renderSize(size);
parts.infoSize.title = `${t('genericSize')}: ${groupThousands(size)} B`;
if (!newUI.enabled) {
parts.oldConfigure.classList.toggle('hidden', !configurable);
parts.oldCheckUpdate.classList.toggle('hidden', !style.updateUrl);
parts.oldUpdate.classList.toggle('hidden', !style.updateUrl);
}
// clear the code to free up some memory
@ -130,8 +140,9 @@ function createStyleElement({style, name: nameLC}) {
const entry = parts.entry.cloneNode(true);
entry.id = ENTRY_ID_PREFIX_RAW + style.id;
entry.styleId = style.id;
entry.styleNameLowerCase = nameLC || name.toLocaleLowerCase() + '\n' + name;
entry.styleNameLC = nameLC;
entry.styleMeta = style;
entry.styleSize = size;
entry.className = parts.entryClassBase + ' ' +
(style.enabled ? 'enabled' : 'disabled') +
(style.updateUrl ? ' updatable' : '') +
@ -153,6 +164,12 @@ function createStyleElement({style, name: nameLC}) {
}
function createTargetsElement({entry, expanded, style = entry.styleMeta}) {
const maxTargets = expanded ? 1000 : newUI.enabled ? newUI.targets : 10;
if (!maxTargets) {
entry._numTargets = 0;
return;
}
const displayed = new Set();
const entryTargets = $('.targets', entry);
const expanderCls = $('.applies-to', entry).classList;
const targets = elementParts.targets.cloneNode(true);
@ -160,8 +177,6 @@ function createTargetsElement({entry, expanded, style = entry.styleMeta}) {
let el = entryTargets.firstElementChild;
let numTargets = 0;
let allTargetsRendered = true;
const maxTargets = expanded ? 1000 : newUI.enabled ? newUI.targets : 10;
const displayed = new Set();
for (const type of TARGET_TYPES) {
for (const section of style.sections) {
for (const targetValue of section[type] || []) {
@ -339,35 +354,40 @@ function padLeft(val, width) {
return ' '.repeat(Math.max(0, width - val.length)) + val;
}
function fitNameColumn(styles) {
const align = 1e9; // required by sort()
const lengths = styles.map(s => align +
(s = s.displayName || s.name || '').length +
s.replace(/[^\u3000-\uFE00]+/g, '').length).sort(); // CJK glyphs are twice as wide
const pick = .8; // for example, .8 = 80% in single line, 20% multiline
const extras = 5; // an average for " UC ", "v1.0.0"
const res = lengths[styles.length * pick | 0] - align + extras;
$.root.style.setProperty('--name-width', res + 'ch');
}
function fitSizeColumn(entries) {
const max = entries.reduce((res, e) => Math.max(res, e.styleSize), 0);
$.root.style.setProperty('--size-width', renderSize(max).length + 'ch');
}
function showStyles(styles = [], matchUrlIds) {
const sorted = sorter.sort({
styles: styles.map(style => {
const name = style.customName || style.name || '';
return {
style,
// sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
name: name.toLocaleLowerCase() + '\n' + name,
};
}),
});
const dummies = styles.map(styleToDummyEntry);
const sorted = sorter.sort(dummies);
let index = 0;
let firstRun = true;
installed.dataset.total = styles.length;
const scrollY = (history.state || {}).scrollY;
const shouldRenderAll = scrollY > window.innerHeight || sessionStore.justEditedStyleId;
const renderBin = document.createDocumentFragment();
if (scrollY) {
renderStyles();
} else {
requestAnimationFrame(renderStyles);
}
fitNameColumn(styles);
fitSizeColumn(dummies);
renderStyles();
function renderStyles() {
const t0 = performance.now();
while (index < sorted.length && (shouldRenderAll || performance.now() - t0 < 20)) {
const info = sorted[index++];
const entry = createStyleElement(info);
if (matchUrlIds && !matchUrlIds.includes(info.style.id)) {
while (index < sorted.length && (shouldRenderAll || performance.now() - t0 < 50)) {
const entry = createStyleElement(sorted[index++]);
if (matchUrlIds && !matchUrlIds.includes(entry.styleMeta.id)) {
entry.classList.add('not-matching');
}
renderBin.appendChild(entry);
@ -388,18 +408,26 @@ function showStyles(styles = [], matchUrlIds) {
}
}
function styleToDummyEntry(style) {
const name = style.customName || style.name || '';
return {
styleMeta: style,
styleSize: calcObjSize(style),
// sort case-insensitively the whole list then sort dupes like `Foo` and `foo` case-sensitively
styleNameLC: name.toLocaleLowerCase() + '\n' + name,
};
}
/* exported switchUI */
function switchUI({styleOnly} = {}) {
const current = {};
const changed = {};
let someChanged = false;
for (const id of newUI.ids) {
const value = prefs.get(newUI.prefKeyForId(id));
newUI.readPrefs(current, (id, value) => {
const valueChanged = value !== newUI[id] && (id === 'enabled' || current.enabled);
current[id] = value;
changed[id] = valueChanged;
someChanged |= valueChanged;
}
});
if (!styleOnly && !someChanged) {
return;

View File

@ -1,4 +1,4 @@
/* global $ $create messageBoxProxy */// dom.js
/* global $ $create dom messageBoxProxy */// dom.js
/* global installed */// manage.js
/* global prefs */
/* global t */// localization.js
@ -6,6 +6,10 @@
const sorter = (() => {
const COL_MIN = 300; // same as options.html
const COL_MAX = 9999; // same as options.html
const COL_PROP = '--columns';
const sorterType = {
alpha: (a, b) => a < b ? -1 : a === b ? 0 : 1,
number: (a, b) => (a || 0) - (b || 0),
@ -14,27 +18,32 @@ const sorter = (() => {
const tagData = {
title: {
text: t('genericTitle'),
parse: ({name}) => name,
parse: v => v.styleNameLC,
sorter: sorterType.alpha,
},
usercss: {
text: 'Usercss',
parse: ({style}) => style.usercssData ? 0 : 1,
parse: v => v.styleMeta.usercssData ? 0 : 1,
sorter: sorterType.number,
},
disabled: {
text: '', // added as either "enabled" or "disabled" by the addOptions function
parse: ({style}) => style.enabled ? 1 : 0,
parse: v => v.styleMeta.enabled ? 1 : 0,
sorter: sorterType.number,
},
dateInstalled: {
text: t('dateInstalled'),
parse: ({style}) => style.installDate,
parse: v => v.styleMeta.installDate,
sorter: sorterType.number,
},
dateUpdated: {
text: t('dateUpdated'),
parse: ({style}) => style.updateDate || style.installDate,
parse: ({styleMeta: s}) => s.updateDate || s.installDate,
sorter: sorterType.number,
},
size: {
text: t('genericSize'),
parse: v => v.styleSize,
sorter: sorterType.number,
},
};
@ -53,6 +62,7 @@ const sorter = (() => {
'disabled,asc, title,asc',
'disabled,desc, title,asc',
'disabled,desc, usercss,asc, title,asc',
'size,desc, title,asc',
'{groupDesc}',
'title,desc',
'usercss,asc, title,desc',
@ -65,12 +75,13 @@ const sorter = (() => {
const getPref = () => prefs.get(ID) || prefs.defaults[ID];
let columns = 1;
let minWidth;
function init() {
prefs.subscribe(ID, sorter.update);
$('#sorter-help').onclick = showHelp;
addOptions();
updateColumnCount();
prefs.subscribe('manage.minColumnWidth', updateColumnWidth, {runNow: true});
}
function addOptions() {
@ -120,7 +131,7 @@ const sorter = (() => {
init,
sort({styles}) {
sort(styles) {
const sortBy = getPref().split(splitRegex);
const len = sortBy.length;
return styles.sort((a, b) => {
@ -140,17 +151,9 @@ const sorter = (() => {
update() {
if (!installed) return;
const current = [...installed.children];
const sorted = sorter.sort({
styles: current.map(entry => ({
entry,
name: entry.styleNameLowerCase,
style: entry.styleMeta,
})),
});
if (current.some((entry, index) => entry !== sorted[index].entry)) {
const renderBin = document.createDocumentFragment();
sorted.forEach(({entry}) => renderBin.appendChild(entry));
installed.appendChild(renderBin);
const sorted = sorter.sort([...current]);
if (current.some((el, i) => el !== sorted[i])) {
installed.append(...sorted);
}
sorter.updateStripes();
},
@ -174,21 +177,40 @@ const sorter = (() => {
};
function updateColumnCount() {
let newValue = 1;
for (let el = $.root.lastElementChild;
el.localName === 'style';
el = el.previousElementSibling) {
if (el.textContent.includes('--columns:')) {
newValue = Math.max(1, getComputedStyle($.root).getPropertyValue('--columns') | 0);
break;
}
}
if (columns !== newValue) {
columns = newValue;
const useStyle = [].some.call($.root.children,
el => el.tagName === 'STYLE' && el.textContent.includes(COL_PROP + ':'));
const v = useStyle ? Math.max(1, getComputedStyle($.root).getPropertyValue(COL_PROP) >> 0)
: minWidth ? onResize()
: columns;
if (columns !== v) {
columns = v;
return true;
}
}
function updateColumnWidth(_, val) {
minWidth = Math.max(val, COL_MIN);
if (val < COL_MAX) {
window.on('resize', onResize);
} else {
window.off('resize', onResize);
$.root.style.removeProperty(COL_PROP);
}
sorter.updateStripes({onlyWhenColumnsChanged: true});
}
function onResize(evt) {
const c = Math.max(1, (window.innerWidth - dom.HWval) / minWidth >> 0);
if (columns !== c) {
$.root.style.setProperty(COL_PROP, c);
if (evt) {
columns = c;
sorter.updateStripes();
}
}
return c;
}
async function showHelp(event) {
event.preventDefault();
messageBoxProxy.show({

View File

@ -1,6 +1,6 @@
{
"name": "Stylus",
"version": "1.5.27",
"version": "1.5.28",
"minimum_chrome_version": "55",
"description": "__MSG_description__",
"homepage_url": "https://add0n.com/stylus.html",

View File

@ -66,7 +66,7 @@
<h1 i18n="optionsCustomizeUpdate"></h1>
<div class="items">
<label>
<span i18n="optionsUpdateInterval">
<span i18n="optionsUpdateInterval" data-clickable="0">
<a i18n="title:optionsUpdateImportNote"
data-cmd="note" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
@ -240,6 +240,10 @@
<div class="block">
<h1 i18n="openManage"></h1>
<div class="items">
<label>
<span i18n="manageMinColumnWidth" data-clickable="9999"></span>
<input id="manage.minColumnWidth" type="number" min="300" max="9999">
</label>
<label>
<span i18n="manageNewUI"></span>
<span class="onoffswitch">
@ -268,7 +272,7 @@
</label>
<label>
<span i18n="manageMaxTargets"></span>
<input id="manage.newUI.targets" type="number" min="1" max="99">
<input id="manage.newUI.targets" type="number" min="0" max="99">
</label>
</div>
</div>

View File

@ -47,6 +47,11 @@ a:hover .svg-icon,
fill: var(--fg);
}
.clickable {
text-decoration: underline dotted;
cursor: pointer;
}
.svg-inline-wrapper .svg-icon {
pointer-events: none;
}

View File

@ -1,7 +1,7 @@
/* global API */// msg.js
/* global prefs */
/* global t */// localization.js
/* global $ $$ getEventKeyName messageBoxProxy setupLivePrefs */// dom.js
/* global $ $$ $create getEventKeyName messageBoxProxy setInputValue setupLivePrefs */// dom.js
/* global
CHROME_POPUP_BORDER_BUG
FIREFOX
@ -44,6 +44,14 @@ $('#reset').onclick = async () => {
}
}
};
$$('[data-clickable]').forEach(el => {
const input = $('input', el.closest('label'));
const value = el.dataset.clickable;
const rx = new RegExp(`\\b(${value})\\b`, 'g');
const onclick = () => setInputValue(input, value);
const parts = elementize(el.textContent, rx, s => $create('span.clickable', {onclick}, s));
el.firstChild.replaceWith(...parts);
});
function customizeHotkeys() {
messageBoxProxy.show({
@ -73,6 +81,10 @@ function customizeHotkeys() {
}
}
function elementize(str, rx, cb) {
return str.split(rx).map((s, i) => i % 2 ? cb(s) : s).filter(Boolean);
}
function enforceInputRange(element) {
const min = Number(element.min);
const max = Number(element.max);

7098
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "Stylus",
"version": "1.5.27",
"version": "1.5.28",
"description": "Redesign the web with Stylus, a user styles manager",
"license": "GPL-3.0-only",
"repository": "openstyles/stylus",
@ -10,7 +10,7 @@
},
"dependencies": {
"@eight04/draggable-list": "^0.3.0",
"codemirror": "5.65.7",
"codemirror": "5.65.8",
"db-to-cloud": "^0.7.0",
"jsonlint": "^1.6.3",
"less-bundle": "github:openstyles/less-bundle#v0.1.0",
@ -27,7 +27,7 @@
"glob": "^8.0.3",
"node-fetch": "^2.6.7",
"sync-version": "^1.0.1",
"web-ext": "^6.8.0"
"web-ext": "^7.2.0"
},
"scripts": {
"lint": "eslint \"**/*.js\" --cache",

View File

@ -98,6 +98,10 @@
</div>
</template>
<template data-id="writeForFrames">
<div id="write-for-frames" tabindex="0">&lt;iframe&gt;...</div>
</template>
<script src="js/polyfill.js"></script>
<script src="js/msg.js"></script>
<script src="js/toolbox.js"></script>
@ -143,7 +147,6 @@
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<div>U</div>
</label>
<a id="write-for-frames" title="&lsaquo;iframe&rsaquo;..." tabindex="0" hidden></a>
<span id="write-style-for" i18n="writeStyleFor"></span>
</div>
</div>

View File

@ -491,6 +491,7 @@ a:hover .svg-icon {
margin-left: .5rem;
}
#write-for-frames::before,
.match .match::before {
content: "";
width: .25rem;
@ -510,33 +511,18 @@ a:hover .svg-icon {
display: none;
}
#write-for-frames:not([hidden]) {
position: relative;
width: 5px;
height: 5px;
display: inline-block;
margin: 0 2px 2px;
--dash: transparent 2px, currentColor 2px, currentColor 3px, transparent 3px;
background: linear-gradient(var(--dash)), linear-gradient(90deg, var(--dash));
#write-for-frames {
cursor: pointer;
margin: 0 0 -.25em .5rem;
color: var(--c50);
transition: color .2s;
}
#write-for-frames:hover {
color: var(--fg);
}
#write-for-frames.expanded {
background: linear-gradient(var(--dash));
}
#write-for-frames::after {
position: absolute;
margin: -2px;
border: 1px solid currentColor;
content: "";
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#write-for-frames:not(.expanded) ~ .match:not([data-frame-id="0"]),
#write-for-frames:not(.expanded) ~ .match .match {
#write-style:not(.expanded) .match:not([data-frame-id="0"]),
#write-style:not(.expanded) .match .match {
display: none;
}

View File

@ -144,10 +144,15 @@ async function initPopup(frames) {
}
frames.forEach(createWriterElement);
Object.assign($('#write-for-frames'), {
onclick: e => e.currentTarget.classList.toggle('expanded'),
hidden: frames.length < 2 || !$('.match .match:not(.dupe)'),
});
if (frames.length > 1 && $('.match .match:not(.dupe)')) {
$('#write-style').append(Object.assign(t.template.writeForFrames, {
onclick() {
this.remove();
$('#write-style').classList.add('expanded');
},
}));
}
const isStore = tabURL.startsWith(URLS.browserWebStore);
if (isStore && !FIREFOX) {

View File

@ -134,6 +134,10 @@
width: 100%;
}
.search-result[data-installed] {
box-shadow: 1px 1px 10px darkcyan;
border-color: darkcyan;
}
.search-result:not([data-installed]) .search-result-actions {
opacity: 0;
transition: opacity .5s;

View File

@ -2,7 +2,7 @@
/* global $entry tabURL */// popup.js
/* global API */// msg.js
/* global Events */
/* global FIREFOX URLS debounce download stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global FIREFOX URLS debounce download isEmptyObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global prefs */
/* global t */// localization.js
'use strict';
@ -27,7 +27,7 @@
/**
* @typedef IndexEntry
* @prop {'uso' | 'uso-android'} f - format
* @prop {Number} i - id
* @prop {Number} i - id, later replaced with string like `uso-123`
* @prop {string} n - name
* @prop {string} c - category
* @prop {Number} u - updatedTime
@ -39,8 +39,8 @@
* @prop {string} sn - screenshotName
* @prop {boolean} sa - screenshotArchived
*
* @prop {boolean} _installed
* @prop {number} _installedStyleId
* @prop {number} _styleId - installed style id
* @prop {boolean} _styleVars - installed style has vars
* @prop {number} _year
*/
/** @type IndexEntry[] */
@ -58,6 +58,8 @@
let displayedPage = 1;
let totalPages = 1;
let ready;
/** @type {?Promise} */
let indexing;
let imgType = '.jpg';
// detect WebP support
@ -71,6 +73,7 @@
const entry = el.closest(RESULT_SEL);
return {entry, result: entry && entry._result};
};
const rid2id = rid => rid.split('-')[1];
Events.searchInline = () => {
calcCategory();
ready = start();
@ -156,18 +159,22 @@
window.on('styleDeleted', ({detail: {style: {id}}}) => {
restoreScrollPosition();
const result = results.find(r => r._installedStyleId === id);
if (result) {
API.uso.pingback(result.i, false);
renderActionButtons(result.i, -1);
const r = results.find(r => r._styleId === id);
if (r) {
if (r.f) API.uso.pingback(rid2id(r.i), false);
delete r._styleId;
renderActionButtons(r.i);
}
});
window.on('styleAdded', async ({detail: {style}}) => {
restoreScrollPosition();
const id = calcId(style) || calcId(await API.styles.get(style.id));
if (id && results.find(r => r.i === id)) {
renderActionButtons(id, style.id);
const ri = await API.styles.getRemoteInfo(style.id);
const r = ri && results.find(r => ri[0] === r.i);
if (r) {
r._styleId = style.id;
r._styleVars = ri[1];
renderActionButtons(ri[0]);
}
});
@ -192,6 +199,13 @@
}
}
function errorIfNoneFound() {
if (!results.length && !$('#search-query').value) {
return indexing ? indexing.then(errorIfNoneFound)
: Promise.reject(t('searchResultNoneFound'));
}
}
async function start({keepYears} = {}) {
resetUI.timer = resetUI.timer || setTimeout(resetUI, 500);
try {
@ -200,19 +214,16 @@
results = await search({retry});
}
if (results.length) {
const installedStyles = await API.styles.getAll();
const allSupportedIds = new Set(installedStyles.map(calcId));
results = results.filter(r => !allSupportedIds.has(r.i));
const info = await API.styles.getRemoteInfo();
for (const r of results) {
[r._styleId, r._styleVars] = info[r.i] || [];
}
}
if (!keepYears) resultsAllYears = results;
renderYears();
render();
dom.list.hidden = !results.length;
if (!results.length && !$('#search-query').value) {
if (index._ready) error(t('searchResultNoneFound'));
} else {
resetUI();
}
await errorIfNoneFound();
resetUI();
} catch (reason) {
error(reason);
@ -325,7 +336,7 @@
function createSearchResultNode(result) {
const entry = t.template.searchResult.cloneNode(true);
const {
i: id,
i: rid,
n: name,
r: rating,
u: updateTime,
@ -337,7 +348,8 @@
sn: shot,
f: fmt,
} = entry._result = result;
entry.id = RESULT_ID_PREFIX + id;
const id = rid2id(rid);
entry.id = RESULT_ID_PREFIX + rid;
// title
Object.assign($('.search-result-title', entry), {
onclick: Events.openURLandHide,
@ -416,22 +428,19 @@
}
}
function renderActionButtons(entry, installedId) {
if (Number(entry)) {
function renderActionButtons(entry) {
if (typeof entry !== 'object') {
entry = $('#' + RESULT_ID_PREFIX + entry);
}
if (!entry) return;
const result = entry._result;
if (typeof installedId === 'number') {
result._installed = installedId > 0;
result._installedStyleId = installedId;
}
const isInstalled = result._installed;
const installedId = result._styleId;
const isInstalled = installedId > 0; // must be boolean for comparisons below
const status = $('.search-result-status', entry).textContent =
isInstalled ? t('clickToUninstall') :
entry.dataset.noImage != null ? t('installButton') :
'';
const notMatching = installedId > 0 && !$entry(installedId);
const notMatching = isInstalled && !$entry(installedId);
if (notMatching !== entry.classList.contains('not-matching')) {
entry.classList.toggle('not-matching');
if (notMatching) {
@ -451,6 +460,7 @@
disabled: notMatching,
});
toggleDataset(entry, 'installed', isInstalled);
toggleDataset(entry, 'customizable', result._styleVars);
}
function renderFullInfo(entry, style) {
@ -464,17 +474,20 @@
textContent: description,
title: description,
});
vars = !isEmptyObj(vars);
entry._result._styleVars = vars;
toggleDataset(entry, 'customizable', vars);
}
function configure() {
const styleEntry = $entry($resultEntry(this).result._installedStyleId);
const styleEntry = $entry($resultEntry(this).result._styleId);
Events.configure.call(this, {target: styleEntry});
}
async function install() {
const {entry, result} = $resultEntry(this);
const {i: id, f: fmt} = result;
const {f: fmt} = result;
const id = rid2id(result.i);
const installButton = $('.search-result-install', entry);
showSpinner(entry);
@ -502,7 +515,7 @@
function uninstall() {
const {entry, result} = $resultEntry(this);
saveScrollPosition(entry);
API.styles.delete(result._installedStyleId);
API.styles.delete(result._styleId);
}
function saveScrollPosition(entry) {
@ -548,16 +561,17 @@
async function fetchIndex() {
const timer = setTimeout(showSpinner, BUSY_DELAY, dom.list);
const jobs = [
[INDEX_URL, json => json.filter(entry => entry.f === 'uso')],
[USW_INDEX_URL, json => json.data],
].map(async ([url, transform]) => {
[INDEX_URL, 'uso', json => json.filter(v => v.f === 'uso')],
[USW_INDEX_URL, 'usw', json => json.data],
].map(async ([url, prefix, transform]) => {
const res = transform(await download(url, {responseType: 'json'}));
for (const v of res) v.i = `${prefix}-${v.i}`;
index = index ? index.concat(res) : res;
if (index !== res) ready = ready.then(start);
});
// TODO: use Promise.allSettled when "minimum_chrome_version" >= 76 and "strict_min_version" >= 71
Promise.all(jobs.map(j => j.catch(e => e))).then(() => {
index._ready = true;
indexing = Promise.all(jobs.map(j => j.catch(e => e))).then(() => {
indexing = null;
});
await Promise.race(jobs);
clearTimeout(timer);
@ -602,17 +616,4 @@
: b[order] - a[order]
) || b.t - a.t;
}
function calcUsoId({md5Url: m, updateUrl}) {
return Number(m && m.match(/\d+|$/)[0]) ||
URLS.extractUsoArchiveId(updateUrl);
}
function calcUswId({updateUrl}) {
return URLS.extractUSwId(updateUrl) || 0;
}
function calcId(style) {
return calcUsoId(style) || calcUswId(style);
}
})();

View File

@ -1,13 +1,12 @@
'use strict';
const fs = require('fs');
const fse = require('fs-extra');
const DIR = '_locales/';
const RX_LNG_CODE = /^\w\w(_\w{2,3})?$/; // like `en` or `en_GB`
const makeFileName = lng => `${DIR}${lng}/messages.json`;
const readLngJson = lng => fse.readJsonSync(makeFileName(lng));
const readLngJson = lng => JSON.parse(fs.readFileSync(makeFileName(lng), 'utf8'));
const sortAlpha = ([a], [b]) => a < b ? -1 : a > b;
const src = readLngJson('en');
@ -63,7 +62,7 @@ function fixLngFile(lng) {
fs.rmSync(`${DIR}${lng}`, {recursive: true, force: true});
err = 'no translations -> deleted';
} else {
fse.outputFileSync(makeFileName(lng), resStr + '\n');
fs.writeFileSync(makeFileName(lng), resStr + '\n', 'utf8');
err = [
numUnknown && `${numUnknown} unknown (dropped)`,
numUntranslated && `${numUntranslated} untranslated (dropped)`,

View File

@ -1,4 +1,4 @@
## codemirror v5.65.7
## codemirror v5.65.8
Files copied from NPM (node_modules):
* addon/comment/comment.js

View File

@ -21,6 +21,7 @@
cm.off("fold", onFold);
cm.off("unfold", onFold);
cm.off("swapDoc", onChange);
cm.off("optionChange", optionChange);
}
if (val) {
cm.state.foldGutter = new State(parseOptions(val));
@ -31,6 +32,7 @@
cm.on("fold", onFold);
cm.on("unfold", onFold);
cm.on("swapDoc", onChange);
cm.on("optionChange", optionChange);
}
});
@ -120,6 +122,10 @@
else cm.foldCode(Pos(line, 0), opts);
}
function optionChange(cm, option) {
if (option == "mode") onChange(cm)
}
function onChange(cm) {
var state = cm.state.foldGutter;
if (!state) return;

View File

@ -7772,7 +7772,7 @@
for (var i = newBreaks.length - 1; i >= 0; i--)
{ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); }
});
option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) {
option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g, function (cm, val, old) {
cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g");
if (old != Init) { cm.refresh(); }
});
@ -9368,6 +9368,7 @@
// Used to work around IE issue with selection being forgotten when focus moves away from textarea
this.hasSelection = false;
this.composing = null;
this.resetting = false;
};
TextareaInput.prototype.init = function (display) {
@ -9500,8 +9501,9 @@
// Reset the input to correspond to the selection (or to be empty,
// when not typing and nothing is selected)
TextareaInput.prototype.reset = function (typing) {
if (this.contextMenuPending || this.composing) { return }
if (this.contextMenuPending || this.composing && typing) { return }
var cm = this.cm;
this.resetting = true;
if (cm.somethingSelected()) {
this.prevInput = "";
var content = cm.getSelection();
@ -9512,6 +9514,7 @@
this.prevInput = this.textarea.value = "";
if (ie && ie_version >= 9) { this.hasSelection = null; }
}
this.resetting = false;
};
TextareaInput.prototype.getField = function () { return this.textarea };
@ -9573,7 +9576,7 @@
// possible when it is clear that nothing happened. hasSelection
// will be the case when there is a lot of text in the textarea,
// in which case reading its value would be expensive.
if (this.contextMenuPending || !cm.state.focused ||
if (this.contextMenuPending || this.resetting || !cm.state.focused ||
(hasSelection(input) && !prevInput && !this.composing) ||
cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq)
{ return false }
@ -9860,7 +9863,7 @@
addLegacyProps(CodeMirror);
CodeMirror.version = "5.65.7";
CodeMirror.version = "5.65.8";
return CodeMirror;