Skip to content

Commit 8367a94

Browse files
authored
Merge pull request #4002 from BookStackApp/color_upgrades
Better application color scheme control
2 parents d7723b3 + 631546a commit 8367a94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+443
-244
lines changed

app/Config/setting-defaults.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,20 @@
1616
'app-editor' => 'wysiwyg',
1717
'app-color' => '#206ea7',
1818
'app-color-light' => 'rgba(32,110,167,0.15)',
19+
'link-color' => '#206ea7',
1920
'bookshelf-color' => '#a94747',
2021
'book-color' => '#077b70',
2122
'chapter-color' => '#af4d0d',
2223
'page-color' => '#206ea7',
2324
'page-draft-color' => '#7e50b1',
25+
'app-color-dark' => '#195785',
26+
'app-color-light-dark' => 'rgba(32,110,167,0.15)',
27+
'link-color-dark' => '#429fe3',
28+
'bookshelf-color-dark' => '#ff5454',
29+
'book-color-dark' => '#389f60',
30+
'chapter-color-dark' => '#ee7a2d',
31+
'page-color-dark' => '#429fe3',
32+
'page-draft-color-dark' => '#a66ce8',
2433
'app-custom-head' => false,
2534
'registration-enabled' => false,
2635

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Support\Facades\DB;
5+
6+
class CopyColorSettingsForDarkMode extends Migration
7+
{
8+
/**
9+
* Run the migrations.
10+
*
11+
* @return void
12+
*/
13+
public function up()
14+
{
15+
$colorSettings = [
16+
'app-color',
17+
'app-color-light',
18+
'bookshelf-color',
19+
'book-color',
20+
'chapter-color',
21+
'page-color',
22+
'page-draft-color',
23+
];
24+
25+
$existing = DB::table('settings')
26+
->whereIn('setting_key', $colorSettings)
27+
->get()->toArray();
28+
29+
$newData = [];
30+
foreach ($existing as $setting) {
31+
$newSetting = (array) $setting;
32+
$newSetting['setting_key'] .= '-dark';
33+
$newData[] = $newSetting;
34+
35+
if ($newSetting['setting_key'] === 'app-color-dark') {
36+
$newSetting['setting_key'] = 'link-color';
37+
$newData[] = $newSetting;
38+
$newSetting['setting_key'] = 'link-color-dark';
39+
$newData[] = $newSetting;
40+
}
41+
}
42+
43+
DB::table('settings')->insert($newData);
44+
}
45+
46+
/**
47+
* Reverse the migrations.
48+
*
49+
* @return void
50+
*/
51+
public function down()
52+
{
53+
$colorSettings = [
54+
'app-color-dark',
55+
'link-color',
56+
'link-color-dark',
57+
'app-color-light-dark',
58+
'bookshelf-color-dark',
59+
'book-color-dark',
60+
'chapter-color-dark',
61+
'page-color-dark',
62+
'page-draft-color-dark',
63+
];
64+
65+
DB::table('settings')
66+
->whereIn('setting_key', $colorSettings)
67+
->delete();
68+
}
69+
}

resources/js/components/attachments.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class Attachments extends Component {
4545
this.stopEdit();
4646
/** @var {Tabs} */
4747
const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
48-
tabs.show('items');
48+
tabs.show('attachment-panel-items');
4949
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
5050
this.list.innerHTML = resp.data;
5151
window.$components.init(this.list);

resources/js/components/image-manager.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,9 @@ export class ImageManager extends Component {
140140
}
141141

142142
setActiveFilterTab(filterName) {
143-
this.filterTabs.forEach(t => t.classList.remove('selected'));
144-
const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName);
145-
if (activeTab) {
146-
activeTab.classList.add('selected');
143+
for (const tab of this.filterTabs) {
144+
const selected = tab.dataset.filter === filterName;
145+
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
147146
}
148147
}
149148

resources/js/components/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export {PagePicker} from "./page-picker.js"
4141
export {PermissionsTable} from "./permissions-table.js"
4242
export {Pointer} from "./pointer.js"
4343
export {Popup} from "./popup.js"
44-
export {SettingAppColorPicker} from "./setting-app-color-picker.js"
44+
export {SettingAppColorScheme} from "./setting-app-color-scheme.js"
4545
export {SettingColorPicker} from "./setting-color-picker.js"
4646
export {SettingHomepageControl} from "./setting-homepage-control.js"
4747
export {ShelfSort} from "./shelf-sort.js"

resources/js/components/setting-app-color-picker.js

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {Component} from "./component";
2+
3+
export class SettingAppColorScheme extends Component {
4+
5+
setup() {
6+
this.container = this.$el;
7+
this.mode = this.$opts.mode;
8+
this.lightContainer = this.$refs.lightContainer;
9+
this.darkContainer = this.$refs.darkContainer;
10+
11+
this.container.addEventListener('tabs-change', event => {
12+
const panel = event.detail.showing;
13+
const newMode = (panel === 'color-scheme-panel-light') ? 'light' : 'dark';
14+
this.handleModeChange(newMode);
15+
});
16+
17+
const onInputChange = (event) => {
18+
this.updateAppColorsFromInputs();
19+
20+
if (event.target.name.startsWith('setting-app-color')) {
21+
this.updateLightForInput(event.target);
22+
}
23+
};
24+
this.container.addEventListener('change', onInputChange);
25+
this.container.addEventListener('input', onInputChange);
26+
}
27+
28+
handleModeChange(newMode) {
29+
this.mode = newMode;
30+
const isDark = (newMode === 'dark');
31+
32+
document.documentElement.classList.toggle('dark-mode', isDark);
33+
this.updateAppColorsFromInputs();
34+
}
35+
36+
updateAppColorsFromInputs() {
37+
const inputContainer = this.mode === 'dark' ? this.darkContainer : this.lightContainer;
38+
const inputs = inputContainer.querySelectorAll('input[type="color"]');
39+
for (const input of inputs) {
40+
const splitName = input.name.split('-');
41+
const colorPos = splitName.indexOf('color');
42+
let cssId = splitName.slice(1, colorPos).join('-');
43+
if (cssId === 'app') {
44+
cssId = 'primary';
45+
}
46+
47+
const varName = '--color-' + cssId;
48+
document.body.style.setProperty(varName, input.value);
49+
}
50+
}
51+
52+
/**
53+
* Update the 'light' app color variant for the given input.
54+
* @param {HTMLInputElement} input
55+
*/
56+
updateLightForInput(input) {
57+
const lightName = input.name.replace('-color', '-color-light');
58+
const hexVal = input.value;
59+
const rgb = this.hexToRgb(hexVal);
60+
const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
61+
62+
console.log(input.name, lightName, hexVal, rgbLightVal)
63+
const lightColorInput = this.container.querySelector(`input[name="${lightName}"][type="hidden"]`);
64+
lightColorInput.value = rgbLightVal;
65+
}
66+
67+
/**
68+
* Covert a hex color code to rgb components.
69+
* @attribution https://stackoverflow.com/a/5624139
70+
* @param {String} hex
71+
* @returns {{r: Number, g: Number, b: Number}}
72+
*/
73+
hexToRgb(hex) {
74+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
75+
return {
76+
r: result ? parseInt(result[1], 16) : 0,
77+
g: result ? parseInt(result[2], 16) : 0,
78+
b: result ? parseInt(result[3], 16) : 0
79+
};
80+
}
81+
82+
}

resources/js/components/setting-color-picker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ export class SettingColorPicker extends Component {
1515

1616
setValue(value) {
1717
this.colorInput.value = value;
18-
this.colorInput.dispatchEvent(new Event('change'));
18+
this.colorInput.dispatchEvent(new Event('change', {bubbles: true}));
1919
}
2020
}

resources/js/components/tabs.js

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,49 @@
1-
import {onSelect} from "../services/dom";
21
import {Component} from "./component";
32

43
/**
54
* Tabs
6-
* Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
5+
* Uses accessible attributes to drive its functionality.
6+
* On tab wrapping element:
7+
* - role=tablist
8+
* On tabs (Should be a button):
9+
* - id
10+
* - role=tab
11+
* - aria-selected=true/false
12+
* - aria-controls=<id-of-panel-section>
13+
* On panels:
14+
* - id
15+
* - tabindex=0
16+
* - role=tabpanel
17+
* - aria-labelledby=<id-of-tab-for-panel>
18+
* - hidden (If not shown by default).
719
*/
820
export class Tabs extends Component {
921

1022
setup() {
11-
this.tabContentsByName = {};
12-
this.tabButtonsByName = {};
13-
this.allContents = [];
14-
this.allButtons = [];
23+
this.container = this.$el;
24+
this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
25+
this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
1526

16-
for (const [key, elems] of Object.entries(this.$manyRefs || {})) {
17-
if (key.startsWith('toggle')) {
18-
const cleanKey = key.replace('toggle', '').toLowerCase();
19-
onSelect(elems, e => this.show(cleanKey));
20-
this.allButtons.push(...elems);
21-
this.tabButtonsByName[cleanKey] = elems;
27+
this.container.addEventListener('click', event => {
28+
const button = event.target.closest('[role="tab"]');
29+
if (button) {
30+
this.show(button.getAttribute('aria-controls'));
2231
}
23-
if (key.startsWith('content')) {
24-
const cleanKey = key.replace('content', '').toLowerCase();
25-
this.tabContentsByName[cleanKey] = elems;
26-
this.allContents.push(...elems);
27-
}
28-
}
32+
});
2933
}
3034

31-
show(key) {
32-
this.allContents.forEach(c => {
33-
c.classList.add('hidden');
34-
c.classList.remove('selected');
35-
});
36-
this.allButtons.forEach(b => b.classList.remove('selected'));
35+
show(sectionId) {
36+
for (const panel of this.panels) {
37+
panel.toggleAttribute('hidden', panel.id !== sectionId);
38+
}
3739

38-
const contents = this.tabContentsByName[key] || [];
39-
const buttons = this.tabButtonsByName[key] || [];
40-
if (contents.length > 0) {
41-
contents.forEach(c => {
42-
c.classList.remove('hidden')
43-
c.classList.add('selected')
44-
});
45-
buttons.forEach(b => b.classList.add('selected'));
40+
for (const tab of this.tabs) {
41+
const tabSection = tab.getAttribute('aria-controls');
42+
const selected = tabSection === sectionId;
43+
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
4644
}
45+
46+
this.$emit('change', {showing: sectionId});
4747
}
4848

4949
}

resources/js/services/util.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function scrollAndHighlightElement(element) {
3434
if (!element) return;
3535
element.scrollIntoView({behavior: 'smooth'});
3636

37-
const color = document.getElementById('custom-styles').getAttribute('data-color-light');
37+
const color = getComputedStyle(document.body).getPropertyValue('--color-primary-light');
3838
const initColor = window.getComputedStyle(element).getPropertyValue('background-color');
3939
element.style.backgroundColor = color;
4040
setTimeout(() => {

0 commit comments

Comments
 (0)