/*
Author: Nriver (https://github.com/nriver/)
Widget: Title Color Picker
Description:
- Enables quick and convenient color selection for note titles.
- Supports preset colors, custom colors, and color removal.
*/
class titleColorPickerWidget extends api.NoteContextAwareWidget {
get position() { return 90; }
get parentWidget() { return 'center-pane'; }
isEnabled() { return super.isEnabled(); }
doRender() {
this.$widget = $(
`<style>
.title-enhancements {
position: relative;
display: flex;
align-items: center;
gap: 10px;
left: -50px;
}
.color-picker-div {
position: relative;
top: -29px;
right: -33px;
margin-right: 39px;
}
.color-picker-button {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid var(--primary-button-border-color);
background-color: #000;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.color-picker-button:hover {
transform: scale(1.15);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
}
.color-picker-popup {
display: none;
position: absolute;
top: 28px;
left: -60px;
background: var(--main-background-color);
border: 1px solid rgba(0, 0, 0, 0.05);
padding: 15px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 150px;
backdrop-filter: blur(1px);
}
.preset-colors {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
justify-content: center;
margin-bottom: 12px;
}
.preset-color {
width: 26px;
height: 26px;
border-radius: 8px;
cursor: pointer;
border: 2px solid var(--button-border-color);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
transition: all 0.2s ease;
position: relative;
}
.preset-color:hover {
transform: scale(1.1);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}
.preset-color:active {
transform: scale(0.95);
}
.color-picker-native {
width: 100%;
height: 34px;
border: 2px solid var(--button-border-color);
padding: 0;
margin: 0 auto 12px;
cursor: pointer;
border-radius: 8px;
display: block;
transition: all 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
-webkit-appearance: none;
appearance: none;
background: transparent;
}
.color-picker-native input {
padding-inline-start: 0px;
}
.color-picker-native:hover {
border-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
}
.color-picker-native::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker-native::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
.color-picker-native::-moz-color-swatch {
border: none;
border-radius: 6px;
}
.remove-color-button {
width: 28px;
height: 28px;
padding: 0;
background-color: #f8f8f8;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
transition: all 0.2s ease;
}
.remove-color-button:hover {
background-color: #ffffff;
border-color: rgba(0, 0, 0, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.remove-color-button svg {
width: 18px;
height: 18px;
stroke: #666;
transition: stroke 0.2s ease;
}
.remove-color-button:hover svg {
stroke: #333;
}
</style>`
);
return this.$widget;
}
async refreshWithNote(note) {
$(document).ready(() => {
const container = $("div.note-split:not(.hidden-ext) .note-title-widget");
const presetColors = config.presetColors;
if (!container.children().hasClass('title-enhancements')) {
const enhancementsHtml = $(
`<div class="title-enhancements">
<div class="color-picker-div">
<div class="color-picker-button" id="color-picker-button"></div>
<div class="color-picker-popup" id="color-picker-popup">
<div class="preset-colors"></div>
<input type="color" class="color-picker-native">
<button class="remove-color-button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
</div>`);
container.append(enhancementsHtml);
const $presetContainer = container.find('.preset-colors');
presetColors.forEach(color => {
const hexColor = namedColorToHex(color);
$presetContainer.append(
`<div class="preset-color" style="background-color: ${hexColor}" data-color="${hexColor}"></div>`
);
});
}
const noteId = note.noteId;
const $colorPickerButton = container.find('.color-picker-button');
const $colorPopup = container.find('.color-picker-popup');
const $presetColors = container.find('.preset-color');
const $nativeColorPicker = container.find('.color-picker-native');
const $removeColorButton = container.find('.remove-color-button');
// Set initial color
let initialColor = "#cccccc";
if (note.hasLabel("color")) {
const existingColor = note.getLabel("color").value;
if (existingColor) initialColor = existingColor;
} else {
const themeColor = getComputedStyle(document.documentElement)
.getPropertyValue('--input-text-color').trim();
if (themeColor && themeColor !== "") initialColor = themeColor;
}
initialColor = normalizeShorthandColor(initialColor);
$colorPickerButton.css('background-color', initialColor);
$nativeColorPicker.val(initialColor);
$("div.note-title-widget.component > input").each(function() {
this.style.setProperty('color', initialColor, 'important');
});
// Show/hide popup on button click
$colorPickerButton.off('click').on('click', function(e) {
e.stopPropagation();
$colorPopup.toggle();
});
// Click on preset colors
$presetColors.off('click').on('click', function() {
const selectedColor = $(this).data('color');
applyColor(selectedColor);
});
// Native color picker
$nativeColorPicker.off('input').on('input', function() {
const selectedColor = $(this).val();
applyColor(selectedColor);
});
// Remove color button
$removeColorButton.off('click').on('click', function() {
removeColor();
});
// Hide popup when clicking outside
$(document).on('click', function(e) {
if (!$colorPopup.is(e.target) &&
$colorPopup.has(e.target).length === 0 &&
!$colorPickerButton.is(e.target)) {
$colorPopup.hide();
}
});
// Function to apply color
function applyColor(color) {
$("div.note-title-widget.component > input").each(function() {
this.style.setProperty('color', color, 'important');
});
$colorPickerButton.css('background-color', color);
$nativeColorPicker.val(color);
api.runAsyncOnBackendWithManualTransactionHandling(async (noteId, color) => {
const currentNote = await api.getNote(noteId);
currentNote.setAttribute("label", "color", color);
currentNote.save();
}, [noteId, color]);
}
// Function to remove color
function removeColor() {
const defaultColor = getComputedStyle(document.documentElement)
.getPropertyValue('--input-text-color').trim() || "#cccccc";
$("div.note-title-widget.component > input").each(function() {
this.style.setProperty('color', defaultColor, 'important');
});
$colorPickerButton.css('background-color', defaultColor);
$nativeColorPicker.val(normalizeShorthandColor(defaultColor));
api.runAsyncOnBackendWithManualTransactionHandling(async (noteId) => {
const currentNote = await api.getNote(noteId);
currentNote.removeAttribute("label", "color");
currentNote.save();
}, [noteId]);
api.refreshIncludedNote(noteId);
api.reloadNotes(noteId);
}
});
}
}
// Function to normalize shorthand colors like #ccc to #cccccc
function normalizeShorthandColor(color) {
// Regular expression to check for Hex shorthand (e.g., #abc)
const hexShorthandRegex = /^#[0-9a-fA-F]{3}$/;
// If the color matches the Hex shorthand format
if (hexShorthandRegex.test(color)) {
// Expand the shorthand format to a full 6-character hex (e.g., #abc -> #aabbcc)
return "#" + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
}
// For other colors (like full hex or named colors), return the color as is
return color;
}
// Function to convert named colors like `green` to hex values
// Native color picker needs this
function namedColorToHex(color) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = color;
return ctx.fillStyle; // Returns the color in the proper hex format
}
module.exports = new titleColorPickerWidget();