mirror of
https://github.com/zadam/trilium.git
synced 2025-11-15 09:45:52 +01:00
Merge branch 'master' into next61
# Conflicts: # src/becca/entities/bnote.js # src/public/app/components/note_context.js # src/public/app/layouts/desktop_layout.js # src/public/app/services/note_content_renderer.js # src/public/app/services/utils.js # src/public/app/widgets/ribbon_widgets/file_properties.js # src/public/app/widgets/ribbon_widgets/note_info_widget.js # src/services/notes.js
This commit is contained in:
@@ -69,18 +69,6 @@ function reload() {
|
||||
require('../services/ws').reloadFrontend();
|
||||
}
|
||||
|
||||
function postProcessEntityUpdate(entityName, entity) {
|
||||
if (entityName === 'notes') {
|
||||
noteUpdated(entity);
|
||||
} else if (entityName === 'branches') {
|
||||
branchUpdated(entity);
|
||||
} else if (entityName === 'attributes') {
|
||||
attributeUpdated(entity);
|
||||
} else if (entityName === 'note_reordering') {
|
||||
noteReorderingUpdated(entity);
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => {
|
||||
if (!becca.loaded) {
|
||||
return;
|
||||
@@ -112,6 +100,25 @@ eventService.subscribeBeccaLoader(eventService.ENTITY_CHANGED, ({entityName, en
|
||||
postProcessEntityUpdate(entityName, entity);
|
||||
});
|
||||
|
||||
/**
|
||||
* This gets run on entity being created or updated.
|
||||
*
|
||||
* @param entityName
|
||||
* @param entityRow - can be a becca entity (change comes from this trilium instance) or just a row (from sync).
|
||||
* Should be therefore treated as a row.
|
||||
*/
|
||||
function postProcessEntityUpdate(entityName, entityRow) {
|
||||
if (entityName === 'notes') {
|
||||
noteUpdated(entityRow);
|
||||
} else if (entityName === 'branches') {
|
||||
branchUpdated(entityRow);
|
||||
} else if (entityName === 'attributes') {
|
||||
attributeUpdated(entityRow);
|
||||
} else if (entityName === 'note_reordering') {
|
||||
noteReorderingUpdated(entityRow);
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribeBeccaLoader([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({entityName, entityId}) => {
|
||||
if (!becca.loaded) {
|
||||
return;
|
||||
@@ -149,6 +156,7 @@ function branchDeleted(branchId) {
|
||||
.filter(parentBranch => parentBranch.branchId !== branch.branchId);
|
||||
|
||||
if (childNote.parents.length > 0) {
|
||||
// subtree notes might lose some inherited attributes
|
||||
childNote.invalidateSubTree();
|
||||
}
|
||||
}
|
||||
@@ -163,8 +171,8 @@ function branchDeleted(branchId) {
|
||||
delete becca.branches[branch.branchId];
|
||||
}
|
||||
|
||||
function noteUpdated(entity) {
|
||||
const note = becca.notes[entity.noteId];
|
||||
function noteUpdated(entityRow) {
|
||||
const note = becca.notes[entityRow.noteId];
|
||||
|
||||
if (note) {
|
||||
// type / mime could have been changed, and they are present in flatTextCache
|
||||
@@ -172,15 +180,19 @@ function noteUpdated(entity) {
|
||||
}
|
||||
}
|
||||
|
||||
function branchUpdated(branch) {
|
||||
const childNote = becca.notes[branch.noteId];
|
||||
function branchUpdated(branchRow) {
|
||||
const childNote = becca.notes[branchRow.noteId];
|
||||
|
||||
if (childNote) {
|
||||
childNote.flatTextCache = null;
|
||||
childNote.sortParents();
|
||||
|
||||
// notes in the subtree can get new inherited attributes
|
||||
// this is in theory needed upon branch creation, but there's no create event for sync changes
|
||||
childNote.invalidateSubTree();
|
||||
}
|
||||
|
||||
const parentNote = becca.notes[branch.parentNoteId];
|
||||
const parentNote = becca.notes[branchRow.parentNoteId];
|
||||
|
||||
if (parentNote) {
|
||||
parentNote.sortChildren();
|
||||
@@ -222,8 +234,10 @@ function attributeDeleted(attributeId) {
|
||||
}
|
||||
}
|
||||
|
||||
function attributeUpdated(attribute) {
|
||||
const note = becca.notes[attribute.noteId];
|
||||
/** @param {BAttribute} attributeRow */
|
||||
function attributeUpdated(attributeRow) {
|
||||
const attribute = becca.attributes[attributeRow.attributeId];
|
||||
const note = becca.notes[attributeRow.noteId];
|
||||
|
||||
if (note) {
|
||||
if (attribute.isAffectingSubtree || note.isInherited()) {
|
||||
|
||||
@@ -6,12 +6,12 @@ const sql = require('../../services/sql');
|
||||
const utils = require('../../services/utils');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const AbstractBeccaEntity = require("./abstract_becca_entity");
|
||||
const BRevision = require("./brevision.js");
|
||||
const BRevision = require("./brevision");
|
||||
const BAttachment = require("./battachment");
|
||||
const TaskContext = require("../../services/task_context");
|
||||
const dayjs = require("dayjs");
|
||||
const utc = require('dayjs/plugin/utc');
|
||||
const eventService = require("../../services/events.js");
|
||||
const eventService = require("../../services/events");
|
||||
dayjs.extend(utc);
|
||||
|
||||
const LABEL = 'label';
|
||||
@@ -87,7 +87,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
this.decrypt();
|
||||
|
||||
/** @type {string|null} */
|
||||
this.flatTextCache = null;
|
||||
this.__flatTextCache = null;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -111,7 +111,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
this.__attributeCache = null;
|
||||
/** @type {BAttribute[]|null}
|
||||
* @private */
|
||||
this.inheritableAttributeCache = null;
|
||||
this.__inheritableAttributeCache = null;
|
||||
|
||||
/** @type {BAttribute[]}
|
||||
* @private */
|
||||
@@ -121,7 +121,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
|
||||
/** @type {BNote[]|null}
|
||||
* @private */
|
||||
this.ancestorCache = null;
|
||||
this.__ancestorCache = null;
|
||||
|
||||
// following attributes are filled during searching from database
|
||||
|
||||
@@ -392,11 +392,11 @@ class BNote extends AbstractBeccaEntity {
|
||||
}
|
||||
}
|
||||
|
||||
this.inheritableAttributeCache = [];
|
||||
this.__inheritableAttributeCache = [];
|
||||
|
||||
for (const attr of this.__attributeCache) {
|
||||
if (attr.isInheritable) {
|
||||
this.inheritableAttributeCache.push(attr);
|
||||
this.__inheritableAttributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,11 +413,11 @@ class BNote extends AbstractBeccaEntity {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
|
||||
if (!this.__inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
|
||||
}
|
||||
|
||||
return this.inheritableAttributeCache;
|
||||
return this.__inheritableAttributeCache;
|
||||
}
|
||||
|
||||
__validateTypeName(type, name) {
|
||||
@@ -751,40 +751,40 @@ class BNote extends AbstractBeccaEntity {
|
||||
* @returns {string} - returns flattened textual representation of note, prefixes and attributes
|
||||
*/
|
||||
getFlatText() {
|
||||
if (!this.flatTextCache) {
|
||||
this.flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
|
||||
if (!this.__flatTextCache) {
|
||||
this.__flatTextCache = `${this.noteId} ${this.type} ${this.mime} `;
|
||||
|
||||
for (const branch of this.parentBranches) {
|
||||
if (branch.prefix) {
|
||||
this.flatTextCache += `${branch.prefix} `;
|
||||
this.__flatTextCache += `${branch.prefix} `;
|
||||
}
|
||||
}
|
||||
|
||||
this.flatTextCache += `${this.title} `;
|
||||
this.__flatTextCache += `${this.title} `;
|
||||
|
||||
for (const attr of this.getAttributes()) {
|
||||
// it's best to use space as separator since spaces are filtered from the search string by the tokenization into words
|
||||
this.flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`;
|
||||
this.__flatTextCache += `${attr.type === 'label' ? '#' : '~'}${attr.name}`;
|
||||
|
||||
if (attr.value) {
|
||||
this.flatTextCache += `=${attr.value}`;
|
||||
this.__flatTextCache += `=${attr.value}`;
|
||||
}
|
||||
|
||||
this.flatTextCache += ' ';
|
||||
this.__flatTextCache += ' ';
|
||||
}
|
||||
|
||||
this.flatTextCache = utils.normalize(this.flatTextCache);
|
||||
this.__flatTextCache = utils.normalize(this.__flatTextCache);
|
||||
}
|
||||
|
||||
return this.flatTextCache;
|
||||
return this.__flatTextCache;
|
||||
}
|
||||
|
||||
invalidateThisCache() {
|
||||
this.flatTextCache = null;
|
||||
this.__flatTextCache = null;
|
||||
|
||||
this.__attributeCache = null;
|
||||
this.inheritableAttributeCache = null;
|
||||
this.ancestorCache = null;
|
||||
this.__inheritableAttributeCache = null;
|
||||
this.__ancestorCache = null;
|
||||
}
|
||||
|
||||
invalidateSubTree(path = []) {
|
||||
@@ -813,24 +813,6 @@ class BNote extends AbstractBeccaEntity {
|
||||
}
|
||||
}
|
||||
|
||||
invalidateSubtreeFlatText() {
|
||||
this.flatTextCache = null;
|
||||
|
||||
for (const childNote of this.children) {
|
||||
childNote.invalidateSubtreeFlatText();
|
||||
}
|
||||
|
||||
for (const targetRelation of this.targetRelations) {
|
||||
if (targetRelation.name === 'template' || targetRelation.name === 'inherit') {
|
||||
const note = targetRelation.note;
|
||||
|
||||
if (note) {
|
||||
note.invalidateSubtreeFlatText();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRelationDefinitions() {
|
||||
return this.getLabels()
|
||||
.filter(l => l.name.startsWith("relation:"));
|
||||
@@ -1021,28 +1003,28 @@ class BNote extends AbstractBeccaEntity {
|
||||
|
||||
/** @returns {BNote[]} */
|
||||
getAncestors() {
|
||||
if (!this.ancestorCache) {
|
||||
if (!this.__ancestorCache) {
|
||||
const noteIds = new Set();
|
||||
this.ancestorCache = [];
|
||||
this.__ancestorCache = [];
|
||||
|
||||
for (const parent of this.parents) {
|
||||
if (noteIds.has(parent.noteId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.ancestorCache.push(parent);
|
||||
this.__ancestorCache.push(parent);
|
||||
noteIds.add(parent.noteId);
|
||||
|
||||
for (const ancestorNote of parent.getAncestors()) {
|
||||
if (!noteIds.has(ancestorNote.noteId)) {
|
||||
this.ancestorCache.push(ancestorNote);
|
||||
this.__ancestorCache.push(ancestorNote);
|
||||
noteIds.add(ancestorNote.noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.ancestorCache;
|
||||
return this.__ancestorCache;
|
||||
}
|
||||
|
||||
/** @returns {string[]} */
|
||||
@@ -1178,7 +1160,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
|
||||
/**
|
||||
* @param {string} [hoistedNoteId='root']
|
||||
* @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]}
|
||||
* @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array<string>, isHidden: boolean}>}
|
||||
*/
|
||||
getSortedNotePathRecords(hoistedNoteId = 'root') {
|
||||
const isHoistedRoot = hoistedNoteId === 'root';
|
||||
@@ -1548,7 +1530,7 @@ class BNote extends AbstractBeccaEntity {
|
||||
if (this.isProtected && !this.isDecrypted && protectedSessionService.isProtectedSessionAvailable()) {
|
||||
try {
|
||||
this.title = protectedSessionService.decryptString(this.title);
|
||||
this.flatTextCache = null;
|
||||
this.__flatTextCache = null;
|
||||
|
||||
this.isDecrypted = true;
|
||||
}
|
||||
|
||||
@@ -40,19 +40,25 @@ function register(router) {
|
||||
}
|
||||
});
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH = {
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH_LABEL = {
|
||||
'value': [v.notNull, v.isString],
|
||||
'position': [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
const ALLOWED_PROPERTIES_FOR_PATCH_RELATION = {
|
||||
'position': [v.notNull, v.isInteger]
|
||||
};
|
||||
|
||||
eu.route(router, 'patch' ,'/etapi/attributes/:attributeId', (req, res, next) => {
|
||||
const attribute = eu.getAndCheckAttribute(req.params.attributeId);
|
||||
|
||||
if (attribute.type === 'relation') {
|
||||
if (attribute.type === 'label') {
|
||||
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_LABEL);
|
||||
} else if (attribute.type === 'relation') {
|
||||
eu.getAndCheckNote(req.body.value);
|
||||
}
|
||||
|
||||
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
|
||||
eu.validateAndPatch(attribute, req.body, ALLOWED_PROPERTIES_FOR_PATCH_RELATION);
|
||||
}
|
||||
|
||||
attribute.save();
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
patch:
|
||||
description: patch a branch identified by the branchId with changes in the body
|
||||
description: patch a branch identified by the branchId with changes in the body. Only prefix and notePosition can be updated. If you want to update other properties, you need to delete the old branch and create a new one.
|
||||
operationId: patchBranchById
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -456,7 +456,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
patch:
|
||||
description: patch a attribute identified by the attributeId with changes in the body
|
||||
description: patch a attribute identified by the attributeId with changes in the body. For labels, only value and position can be updated. For relations, only position can be updated. If you want to modify other properties, you need to delete the old attribute and create a new one.
|
||||
operationId: patchAttributeById
|
||||
requestBody:
|
||||
required: true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @param {BNote} note */
|
||||
function mapNoteToPojo(note) {
|
||||
return {
|
||||
noteId: note.noteId,
|
||||
@@ -18,6 +19,7 @@ function mapNoteToPojo(note) {
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {BBranch} branch */
|
||||
function mapBranchToPojo(branch) {
|
||||
return {
|
||||
branchId: branch.branchId,
|
||||
@@ -30,6 +32,7 @@ function mapBranchToPojo(branch) {
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {BAttribute} attr */
|
||||
function mapAttributeToPojo(attr) {
|
||||
return {
|
||||
attributeId: attr.attributeId,
|
||||
|
||||
@@ -171,9 +171,12 @@ class NoteContext extends Component {
|
||||
}
|
||||
|
||||
getPojoState() {
|
||||
if (!this.notePath && this.hoistedNoteId === 'root') {
|
||||
// keeping empty hoisted tab is esp. important for mobile (e.g., opened launcher config)
|
||||
return null;
|
||||
if (this.hoistedNoteId !== 'root') {
|
||||
// keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config)
|
||||
|
||||
if (!this.notePath && this.getSubContexts().length === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -477,16 +477,23 @@ export default class TabManager extends Component {
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
noteContextReorderEvent({ntxIdsInOrder}) {
|
||||
const order = {};
|
||||
let i = 0;
|
||||
|
||||
for (const ntxId of ntxIdsInOrder) {
|
||||
order[ntxId] = i++;
|
||||
}
|
||||
noteContextReorderEvent({ntxIdsInOrder, oldMainNtxId, newMainNtxId}) {
|
||||
const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i]));
|
||||
|
||||
this.children.sort((a, b) => order[a.ntxId] < order[b.ntxId] ? -1 : 1);
|
||||
|
||||
if (oldMainNtxId && newMainNtxId) {
|
||||
this.children.forEach(c => {
|
||||
if (c.ntxId === newMainNtxId) {
|
||||
// new main context has null mainNtxId
|
||||
c.mainNtxId = null;
|
||||
} else if (c.ntxId === oldMainNtxId || c.mainNtxId === oldMainNtxId) {
|
||||
// old main context or subcontexts all have the new mainNtxId
|
||||
c.mainNtxId = newMainNtxId;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.tabsUpdate.scheduleUpdate();
|
||||
}
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ class FNote {
|
||||
|
||||
/**
|
||||
* @param {string} [hoistedNoteId='root']
|
||||
* @return {{isArchived: boolean, isInHoistedSubTree: boolean, notePath: string[], isHidden: boolean}[]}
|
||||
* @return {Array<{isArchived: boolean, isInHoistedSubTree: boolean, notePath: Array<string>, isHidden: boolean}>}
|
||||
*/
|
||||
getSortedNotePathRecords(hoistedNoteId = 'root') {
|
||||
const isHoistedRoot = hoistedNoteId === 'root';
|
||||
@@ -431,7 +431,7 @@ class FNote {
|
||||
for (const parentNote of this.getParentNotes()) {
|
||||
if (parentNote.noteId === 'root') {
|
||||
return false;
|
||||
} else if (parentNote.noteId === '_hidden') {
|
||||
} else if (parentNote.noteId === '_hidden' || parentNote.type === 'search') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import FindWidget from "../widgets/find.js";
|
||||
import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
|
||||
import AboutDialog from "../widgets/dialogs/about.js";
|
||||
import HelpDialog from "../widgets/dialogs/help.js";
|
||||
@@ -75,6 +76,7 @@ import CodeButtonsWidget from "../widgets/floating_buttons/code_buttons.js";
|
||||
import ApiLogWidget from "../widgets/api_log.js";
|
||||
import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating_buttons_button.js";
|
||||
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
@@ -124,6 +126,8 @@ export default class DesktopLayout {
|
||||
.child(new NoteIconWidget())
|
||||
.child(new NoteTitleWidget())
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(new MovePaneButton(true))
|
||||
.child(new MovePaneButton(false))
|
||||
.child(new ClosePaneButton())
|
||||
.child(new CreatePaneButton())
|
||||
)
|
||||
@@ -182,6 +186,7 @@ export default class DesktopLayout {
|
||||
)
|
||||
.child(new RightPaneContainer()
|
||||
.child(new TocWidget())
|
||||
.child(new HighlightsListWidget())
|
||||
.child(...this.customWidgets.get('right-pane'))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -483,6 +483,13 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
*/
|
||||
this.randomString = utils.randomString;
|
||||
|
||||
/**
|
||||
* @method
|
||||
* @param {int} size in bytes
|
||||
* @return {string} formatted string
|
||||
*/
|
||||
this.formatNoteSize = utils.formatNoteSize;
|
||||
|
||||
this.logMessages = {};
|
||||
this.logSpacedUpdates = {};
|
||||
|
||||
|
||||
0
src/public/app/services/note_content_renderer.js
Normal file
0
src/public/app/services/note_content_renderer.js
Normal file
@@ -522,6 +522,17 @@ function copyHtmlToClipboard(content) {
|
||||
navigator.clipboard.write([clipboardItem]);
|
||||
}
|
||||
|
||||
function formatNoteSize(size) {
|
||||
size = Math.max(Math.round(size / 1024), 1);
|
||||
|
||||
if (size < 1024) {
|
||||
return `${size} KiB`;
|
||||
}
|
||||
else {
|
||||
return `${Math.round(size / 102.4) / 10} MiB`;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
parseDate,
|
||||
@@ -567,6 +578,8 @@ export default {
|
||||
isValidAttributeName,
|
||||
sleep,
|
||||
escapeRegExp,
|
||||
formatNoteSize,
|
||||
escapeRegExp,
|
||||
areObjectsEqual,
|
||||
copyHtmlToClipboard
|
||||
};
|
||||
|
||||
@@ -7,6 +7,10 @@ export default class ClosePaneButton extends OnClickButtonWidget {
|
||||
&& this.noteContext && !!this.noteContext.mainNtxId;
|
||||
}
|
||||
|
||||
async noteContextReorderEvent({ntxIdsInOrder}) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
|
||||
47
src/public/app/widgets/buttons/move_pane_button.js
Normal file
47
src/public/app/widgets/buttons/move_pane_button.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import OnClickButtonWidget from "./onclick_button.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
|
||||
export default class MovePaneButton extends OnClickButtonWidget {
|
||||
constructor(isMovingLeft) {
|
||||
super();
|
||||
|
||||
this.isMovingLeft = isMovingLeft;
|
||||
|
||||
this.icon(isMovingLeft ? "bx-chevron-left" : "bx-chevron-right")
|
||||
.title(isMovingLeft ? "Move left" : "Move right")
|
||||
.titlePlacement("bottom")
|
||||
.onClick(async (widget, e) => {
|
||||
e.stopPropagation();
|
||||
widget.triggerCommand("moveThisNoteSplit", {ntxId: widget.getClosestNtxId(), isMovingLeft: this.isMovingLeft});
|
||||
})
|
||||
.class("icon-action");
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
if (!super.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isMovingLeft) {
|
||||
// movable if the current context is not a main context, i.e. non-null mainNtxId
|
||||
return !!this.noteContext?.mainNtxId;
|
||||
} else {
|
||||
const currentIndex = appContext.tabManager.noteContexts.findIndex(c => c.ntxId === this.ntxId);
|
||||
const nextContext = appContext.tabManager.noteContexts[currentIndex + 1];
|
||||
// movable if the next context is not null and not a main context, i.e. non-null mainNtxId
|
||||
return !!nextContext?.mainNtxId;
|
||||
}
|
||||
}
|
||||
|
||||
async noteContextRemovedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
async newNoteContextCreatedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
async noteContextReorderEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,50 @@ export default class SplitNoteContainer extends FlexContainer {
|
||||
appContext.tabManager.removeNoteContext(ntxId);
|
||||
}
|
||||
|
||||
async moveThisNoteSplitCommand({ntxId, isMovingLeft}) {
|
||||
if (!ntxId) {
|
||||
logError("empty ntxId!");
|
||||
return;
|
||||
}
|
||||
|
||||
const contexts = appContext.tabManager.noteContexts;
|
||||
|
||||
const currentIndex = contexts.findIndex(c => c.ntxId === ntxId);
|
||||
const leftIndex = isMovingLeft ? currentIndex - 1 : currentIndex;
|
||||
|
||||
if (currentIndex === -1 || leftIndex < 0 || leftIndex + 1 >= contexts.length) {
|
||||
logError(`invalid context! currentIndex: ${currentIndex}, leftIndex: ${leftIndex}, contexts.length: ${contexts.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (contexts[leftIndex].isEmpty() && contexts[leftIndex + 1].isEmpty()) {
|
||||
// no op
|
||||
return;
|
||||
}
|
||||
|
||||
const ntxIds = contexts.map(c => c.ntxId);
|
||||
const newNtxIds = [
|
||||
...ntxIds.slice(0, leftIndex),
|
||||
ntxIds[leftIndex + 1],
|
||||
ntxIds[leftIndex],
|
||||
...ntxIds.slice(leftIndex + 2),
|
||||
];
|
||||
const isChangingMainContext = !contexts[leftIndex].mainNtxId;
|
||||
|
||||
this.triggerCommand("noteContextReorder", {
|
||||
ntxIdsInOrder: newNtxIds,
|
||||
oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null,
|
||||
newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1]: null,
|
||||
});
|
||||
|
||||
// reorder the note context widgets
|
||||
this.$widget.find(`[data-ntx-id="${ntxIds[leftIndex]}"]`)
|
||||
.insertAfter(this.$widget.find(`[data-ntx-id="${ntxIds[leftIndex + 1]}"]`));
|
||||
|
||||
// activate context that now contains the original note
|
||||
await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]);
|
||||
}
|
||||
|
||||
activeContextChangedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
257
src/public/app/widgets/highlights_list.js
Normal file
257
src/public/app/widgets/highlights_list.js
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* Widget: Show highlighted text in the right pane
|
||||
*
|
||||
* By design, there's no support for nonsensical or malformed constructs:
|
||||
* - For example, if there is a formula in the middle of the highlighted text, the two ends of the formula will be regarded as two entries
|
||||
*/
|
||||
|
||||
import attributeService from "../services/attributes.js";
|
||||
import RightPanelWidget from "./right_panel_widget.js";
|
||||
import options from "../services/options.js";
|
||||
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
||||
|
||||
const TPL = `<div class="highlists-list-widget">
|
||||
<style>
|
||||
.highlists-list-widget {
|
||||
padding: 10px;
|
||||
contain: none;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlists-list > ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.highlists-list li {
|
||||
cursor: pointer;
|
||||
margin-bottom: 3px;
|
||||
text-align: justify;
|
||||
text-justify: distribute;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.highlists-list li:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close-highlists-list {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="highlists-list"></span>
|
||||
</div>`;
|
||||
|
||||
export default class HighlightsListWidget extends RightPanelWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.closeHltButton = new CloseHltButton();
|
||||
this.child(this.closeHltButton);
|
||||
}
|
||||
|
||||
get widgetTitle() {
|
||||
return "Highlighted Text";
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled()
|
||||
&& this.note.type === 'text'
|
||||
&& !this.noteContext.viewScope.highlightedTextTemporarilyHidden
|
||||
&& this.noteContext.viewScope.viewMode === 'default';
|
||||
}
|
||||
|
||||
async doRenderBody() {
|
||||
this.$body.empty().append($(TPL));
|
||||
this.$highlightsList = this.$body.find('.highlists-list');
|
||||
this.$body.find('.highlists-list-widget').append(this.closeHltButton.render());
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
/* The reason for adding highlightedTextPreviousVisible is to record whether the previous state
|
||||
of the highlightedText is hidden or displayed, and then let it be displayed/hidden at the initial time.
|
||||
If there is no such value, when the right panel needs to display toc but not highlighttext,
|
||||
every time the note content is changed, highlighttext Widget will appear and then close immediately,
|
||||
because getHlt function will consume time */
|
||||
if (this.noteContext.viewScope.highlightedTextPreviousVisible) {
|
||||
this.toggleInt(true);
|
||||
} else {
|
||||
this.toggleInt(false);
|
||||
}
|
||||
|
||||
const optionsHlt = JSON.parse(options.get('highlightedText'));
|
||||
|
||||
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHlt) {
|
||||
this.toggleInt(false);
|
||||
this.triggerCommand("reEvaluateRightPaneVisibility");
|
||||
return;
|
||||
}
|
||||
|
||||
let $highlightsList = "", hltLiCount = -1;
|
||||
// Check for type text unconditionally in case alwaysShowWidget is set
|
||||
if (this.note.type === 'text') {
|
||||
const {content} = await note.getNoteComplement();
|
||||
({$highlightsList, hltLiCount} = this.getHighlightList(content, optionsHlt));
|
||||
}
|
||||
this.$highlightsList.empty().append($highlightsList);
|
||||
if (hltLiCount > 0) {
|
||||
this.toggleInt(true);
|
||||
this.noteContext.viewScope.highlightedTextPreviousVisible = true;
|
||||
} else {
|
||||
this.toggleInt(false);
|
||||
this.noteContext.viewScope.highlightedTextPreviousVisible = false;
|
||||
}
|
||||
|
||||
this.triggerCommand("reEvaluateRightPaneVisibility");
|
||||
}
|
||||
|
||||
getHighlightList(content, optionsHlt) {
|
||||
// matches a span containing background-color
|
||||
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
|
||||
// matches a span containing color
|
||||
const regex2 = /<span[^>]*style\s*=\s*[^>]*[^-]color:[^>]*?>[\s\S]*?<\/span>/gi;
|
||||
// match italics
|
||||
const regex3 = /<i>[\s\S]*?<\/i>/gi;
|
||||
// match bold
|
||||
const regex4 = /<strong>[\s\S]*?<\/strong>/gi;
|
||||
// match underline
|
||||
const regex5 = /<u>[\s\S]*?<\/u>/g;
|
||||
// Possible values in optionsHlt: '["bold","italic","underline","color","bgColor"]'
|
||||
// element priority: span>i>strong>u
|
||||
let findSubStr = "", combinedRegexStr = "";
|
||||
if (optionsHlt.includes("bgColor")) {
|
||||
findSubStr += `,span[style*="background-color"]`;
|
||||
combinedRegexStr += `|${regex1.source}`;
|
||||
}
|
||||
if (optionsHlt.includes("color")) {
|
||||
findSubStr += `,span[style*="color"]`;
|
||||
combinedRegexStr += `|${regex2.source}`;
|
||||
}
|
||||
if (optionsHlt.includes("italic")) {
|
||||
findSubStr += `,i`;
|
||||
combinedRegexStr += `|${regex3.source}`;
|
||||
}
|
||||
if (optionsHlt.indexOf("bold")) {
|
||||
findSubStr += `,strong`;
|
||||
combinedRegexStr += `|${regex4.source}`;
|
||||
}
|
||||
if (optionsHlt.includes("underline")) {
|
||||
findSubStr += `,u`;
|
||||
combinedRegexStr += `|${regex5.source}`;
|
||||
}
|
||||
|
||||
findSubStr = findSubStr.substring(1)
|
||||
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
|
||||
const combinedRegex = new RegExp(combinedRegexStr, 'gi');
|
||||
const $highlightsList = $("<ol>");
|
||||
let prevEndIndex = -1, hltLiCount = 0;
|
||||
for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) {
|
||||
const subHtml = match[0];
|
||||
const startIndex = match.index;
|
||||
const endIndex = combinedRegex.lastIndex;
|
||||
if (prevEndIndex !== -1 && startIndex === prevEndIndex) {
|
||||
// If the previous element is connected to this element in HTML, then concatenate them into one.
|
||||
$highlightsList.children().last().append(subHtml);
|
||||
} else {
|
||||
// TODO: can't be done with $(subHtml).text()?
|
||||
const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
|
||||
|
||||
if (hasText) {
|
||||
$highlightsList.append(
|
||||
$('<li>')
|
||||
.html(subHtml)
|
||||
.on("click", () => this.jumpToHighlightedText(findSubStr, hltIndex))
|
||||
);
|
||||
|
||||
hltLiCount++;
|
||||
} else {
|
||||
// hide li if its text content is empty
|
||||
continue;
|
||||
}
|
||||
}
|
||||
prevEndIndex = endIndex;
|
||||
}
|
||||
return {
|
||||
$highlightsList,
|
||||
hltLiCount
|
||||
};
|
||||
}
|
||||
|
||||
async jumpToHighlightedText(findSubStr, itemIndex) {
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
let targetElement;
|
||||
if (isReadOnly) {
|
||||
const $container = await this.noteContext.getContentElement();
|
||||
targetElement = $container.find(findSubStr).filter(function () {
|
||||
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
||||
let color = this.style.color;
|
||||
return !($(this).prop('tagName') === "SPAN" && color === "");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}).filter(function () {
|
||||
return $(this).parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent().parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent().parent().parent(findSubStr).length === 0;
|
||||
})
|
||||
} else {
|
||||
const textEditor = await this.noteContext.getTextEditor();
|
||||
targetElement = $(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function () {
|
||||
// When finding span[style*="color"] but not looking for span[style*="background-color"],
|
||||
// the background-color error will be regarded as color, so it needs to be filtered
|
||||
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
||||
let color = this.style.color;
|
||||
return !($(this).prop('tagName') === "SPAN" && color === "");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}).filter(function () {
|
||||
// Need to filter out the child elements of the element that has been found
|
||||
return $(this).parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent().parent(findSubStr).length === 0
|
||||
&& $(this).parent().parent().parent().parent(findSubStr).length === 0;
|
||||
})
|
||||
}
|
||||
targetElement[itemIndex].scrollIntoView({
|
||||
behavior: "smooth", block: "center"
|
||||
});
|
||||
}
|
||||
|
||||
async closeHltCommand() {
|
||||
this.noteContext.viewScope.highlightedTextTemporarilyHidden = true;
|
||||
await this.refresh();
|
||||
this.triggerCommand('reEvaluateRightPaneVisibility');
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
await this.refresh();
|
||||
} else if (loadResults.getAttributes().find(attr => attr.type === 'label'
|
||||
&& (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
|
||||
&& attributeService.isAffecting(attr, this.note))) {
|
||||
await this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CloseHltButton extends OnClickButtonWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.icon("bx-x")
|
||||
.title("Close HighlightedTextWidget")
|
||||
.titlePlacement("bottom")
|
||||
.onClick((widget, e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
widget.triggerCommand("closeHlt");
|
||||
})
|
||||
.class("icon-action close-highlists-list");
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export default class FilePropertiesWidget extends NoteContextAwareWidget {
|
||||
|
||||
const blob = await this.note.getBlob();
|
||||
|
||||
this.$fileSize.text(`${blob.contentLength} bytes`);
|
||||
this.$fileSize.text(utils.formatNoteSize(blob.contentLength));
|
||||
|
||||
// open doesn't work for protected notes since it works through a browser which isn't in protected session
|
||||
this.$openButton.toggle(!note.isProtected);
|
||||
|
||||
@@ -106,12 +106,12 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
|
||||
this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
|
||||
|
||||
const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
|
||||
this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
|
||||
this.$noteSize.text(utils.formatNoteSize(noteSizeResp.noteSize));
|
||||
|
||||
const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`);
|
||||
|
||||
if (subTreeResp.subTreeNoteCount > 1) {
|
||||
this.$subTreeSize.text(`(subtree size: ${utils.formatSize(subTreeResp.subTreeSize)} in ${subTreeResp.subTreeNoteCount} notes)`);
|
||||
this.$subTreeSize.text(`(subtree size: ${utils.formatNoteSize(subTreeResp.subTreeSize)} in ${subTreeResp.subTreeNoteCount} notes)`);
|
||||
}
|
||||
else {
|
||||
this.$subTreeSize.text("");
|
||||
|
||||
@@ -609,6 +609,17 @@ export default class TabRowWidget extends BasicWidget {
|
||||
this.updateTabById(noteContext.mainNtxId || noteContext.ntxId);
|
||||
}
|
||||
|
||||
noteContextReorderEvent({oldMainNtxId, newMainNtxId}) {
|
||||
if (!oldMainNtxId || !newMainNtxId) {
|
||||
// no need to update tab row
|
||||
return;
|
||||
}
|
||||
|
||||
// update tab id for the new main context
|
||||
this.getTabById(oldMainNtxId).attr("data-ntx-id", newMainNtxId);
|
||||
this.updateTabById(newMainNtxId);
|
||||
}
|
||||
|
||||
updateTabById(ntxId) {
|
||||
const $tab = this.getTabById(ntxId);
|
||||
|
||||
|
||||
@@ -30,9 +30,13 @@ const TPL = `
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.title-bar-buttons .top-btn.active{
|
||||
background-color:var(--accented-background-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->
|
||||
<div class="top-btn" title="Keep this window on top. "><button class="btn bx bx-pin"></button></div>
|
||||
<div class="minimize-btn"><button class="btn bx bx-minus"></button></div>
|
||||
<div class="maximize-btn"><button class="btn bx bx-checkbox"></button></div>
|
||||
<div class="close-btn"><button class="btn bx bx-x"></button></div>
|
||||
@@ -47,10 +51,34 @@ export default class TitleBarButtonsWidget extends BasicWidget {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
const $topBtn = this.$widget.find(".top-btn");
|
||||
const $minimizeBtn = this.$widget.find(".minimize-btn");
|
||||
const $maximizeBtn = this.$widget.find(".maximize-btn");
|
||||
const $closeBtn = this.$widget.find(".close-btn");
|
||||
|
||||
// When the window is restarted, the window will not be reset when it is set to the top,
|
||||
// so get the window status and set the icon background
|
||||
setTimeout(() => {
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
if (remote.BrowserWindow.getFocusedWindow()?.isAlwaysOnTop()) {
|
||||
$topBtn.addClass('active');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
$topBtn.on('click', () => {
|
||||
$topBtn.trigger('blur');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop()
|
||||
if (isAlwaysOnTop) {
|
||||
focusedWindow.setAlwaysOnTop(false)
|
||||
$topBtn.removeClass('active');
|
||||
} else {
|
||||
focusedWindow.setAlwaysOnTop(true);
|
||||
$topBtn.addClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
$minimizeBtn.on('click', () => {
|
||||
$minimizeBtn.trigger('blur');
|
||||
const remote = utils.dynamicRequire('@electron/remote');
|
||||
|
||||
@@ -38,6 +38,10 @@ const TPL = `<div class="toc-widget">
|
||||
|
||||
.toc li {
|
||||
cursor: pointer;
|
||||
text-align: justify;
|
||||
text-justify: distribute;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.toc li:hover {
|
||||
@@ -80,6 +84,16 @@ export default class TocWidget extends RightPanelWidget {
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
/*The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed,
|
||||
* and then let it be displayed/hidden at the initial time. If there is no such value,
|
||||
* when the right panel needs to display highlighttext but not toc, every time the note content is changed,
|
||||
* toc will appear and then close immediately, because getToc(html) function will consume time*/
|
||||
if (this.noteContext.viewScope.tocPreviousVisible ==true){
|
||||
this.toggleInt(true);
|
||||
}else{
|
||||
this.toggleInt(false);
|
||||
}
|
||||
|
||||
const tocLabel = note.getLabel('toc');
|
||||
|
||||
if (tocLabel?.value === 'hide') {
|
||||
@@ -96,10 +110,13 @@ export default class TocWidget extends RightPanelWidget {
|
||||
}
|
||||
|
||||
this.$toc.html($toc);
|
||||
this.toggleInt(
|
||||
["", "show"].includes(tocLabel?.value)
|
||||
|| headingCount >= options.getInt('minTocHeadings')
|
||||
);
|
||||
if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt('minTocHeadings')){
|
||||
this.toggleInt(true);
|
||||
this.noteContext.viewScope.tocPreviousVisible=true;
|
||||
}else{
|
||||
this.toggleInt(false);
|
||||
this.noteContext.viewScope.tocPreviousVisible=false;
|
||||
}
|
||||
|
||||
this.triggerCommand("reEvaluateRightPaneVisibility");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
|
||||
import KeyboardShortcutsOptions from "./options/shortcuts.js";
|
||||
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
|
||||
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
|
||||
import HighlightedTextOptions from "./options/text_notes/highlighted_text.js";
|
||||
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
|
||||
import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
|
||||
import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
|
||||
@@ -62,6 +63,7 @@ const CONTENT_WIDGETS = {
|
||||
_optionsTextNotes: [
|
||||
HeadingStyleOptions,
|
||||
TableOfContentsOptions,
|
||||
HighlightedTextOptions,
|
||||
TextAutoReadOnlySizeOptions
|
||||
],
|
||||
_optionsCodeNotes: [
|
||||
|
||||
@@ -116,11 +116,11 @@ export default class KeyboardShortcutsOptions extends OptionsWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
$table.find('input.form-control').each(function() {
|
||||
const defaultShortcuts = this.$widget.find(this).attr('data-default-keyboard-shortcuts');
|
||||
$table.find('input.form-control').each((_index, el) => {
|
||||
const defaultShortcuts = this.$widget.find(el).attr('data-default-keyboard-shortcuts');
|
||||
|
||||
if (this.$widget.find(this).val() !== defaultShortcuts) {
|
||||
this.$widget.find(this)
|
||||
if (this.$widget.find(el).val() !== defaultShortcuts) {
|
||||
this.$widget.find(el)
|
||||
.val(defaultShortcuts)
|
||||
.trigger('change');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>Highlighted Text</h4>
|
||||
|
||||
<p>You can customize the highlighted text displayed in the right panel:</p>
|
||||
|
||||
</div>
|
||||
<label><input type="checkbox" class="highlighted-text-check" value="bold"> Bold font </label>
|
||||
<label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font </label>
|
||||
<label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font </label>
|
||||
<label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color </label>
|
||||
<label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color </label>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class HighlightedTextOptions extends OptionsWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$hlt = this.$widget.find("input.highlighted-text-check");
|
||||
this.$hlt.on('change', () => {
|
||||
const hltVals = this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function () {
|
||||
return this.value;
|
||||
}).get();
|
||||
this.updateOption('highlightedText', JSON.stringify(hltVals));
|
||||
});
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
const hltVals = JSON.parse(options.highlightedText);
|
||||
this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
|
||||
if ($.inArray($(this).val(), hltVals) !== -1) {
|
||||
$(this).prop("checked", true);
|
||||
} else {
|
||||
$(this).prop("checked", false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'compressImages',
|
||||
'downloadImagesAutomatically',
|
||||
'minTocHeadings',
|
||||
'highlightedText',
|
||||
'checkForUpdates',
|
||||
'disableTray',
|
||||
'eraseUnusedAttachmentsAfterSeconds',
|
||||
|
||||
@@ -60,11 +60,10 @@ function deriveMime(type, mime) {
|
||||
* @param {BNote} childNote
|
||||
*/
|
||||
function copyChildAttributes(parentNote, childNote) {
|
||||
const hasAlreadyTemplate = childNote.hasRelation('template');
|
||||
|
||||
for (const attr of parentNote.getAttributes()) {
|
||||
if (attr.name.startsWith("child:")) {
|
||||
const name = attr.name.substr(6);
|
||||
const hasAlreadyTemplate = childNote.hasRelation('template');
|
||||
|
||||
if (hasAlreadyTemplate && attr.type === 'relation' && name === 'template') {
|
||||
// if the note already has a template, it means the template was chosen by the user explicitly
|
||||
@@ -181,7 +180,7 @@ function createNewNote(params) {
|
||||
|
||||
// TODO: think about what can happen if the note already exists with the forced ID
|
||||
// I guess on DB it's going to be fine, but becca references between entities
|
||||
// might get messed up (two Note instance for the same ID existing in the references)
|
||||
// might get messed up (two note instances for the same ID existing in the references)
|
||||
note = new BNote({
|
||||
noteId: params.noteId, // optionally can force specific noteId
|
||||
title: params.title,
|
||||
@@ -202,7 +201,7 @@ function createNewNote(params) {
|
||||
}
|
||||
finally {
|
||||
if (!isEntityEventsDisabled) {
|
||||
// re-enable entity events only if there were previously enabled
|
||||
// re-enable entity events only if they were previously enabled
|
||||
// (they can be disabled in case of import)
|
||||
cls.enableEntityEvents();
|
||||
}
|
||||
@@ -222,22 +221,14 @@ function createNewNote(params) {
|
||||
|
||||
copyChildAttributes(parentNote, note);
|
||||
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'notes', entity: note });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'notes', entity: note });
|
||||
triggerNoteTitleChanged(note);
|
||||
|
||||
eventService.emit(eventService.ENTITY_CREATED, {
|
||||
entityName: 'notes',
|
||||
entity: note
|
||||
});
|
||||
|
||||
eventService.emit(eventService.ENTITY_CREATED, {
|
||||
entityName: 'branches',
|
||||
entity: branch
|
||||
});
|
||||
|
||||
eventService.emit(eventService.CHILD_NOTE_CREATED, {
|
||||
childNote: note,
|
||||
parentNote: parentNote
|
||||
});
|
||||
// blobs doesn't use "created" event
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'blobs', entity: note });
|
||||
eventService.emit(eventService.ENTITY_CREATED, { entityName: 'branches', entity: branch });
|
||||
eventService.emit(eventService.ENTITY_CHANGED, { entityName: 'branches', entity: branch });
|
||||
eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote: note, parentNote: parentNote });
|
||||
|
||||
log.info(`Created new note '${note.noteId}', branch '${branch.branchId}' of type '${note.type}', mime '${note.mime}'`);
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ const defaultOptions = [
|
||||
{ name: 'compressImages', value: 'true', isSynced: true },
|
||||
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
|
||||
{ name: 'minTocHeadings', value: '5', isSynced: true },
|
||||
{ name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
|
||||
{ name: 'checkForUpdates', value: 'true', isSynced: true },
|
||||
{ name: 'disableTray', value: 'false', isSynced: false },
|
||||
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },
|
||||
|
||||
@@ -42,7 +42,7 @@ class SNote extends AbstractShacaEntity {
|
||||
/** @param {SAttribute[]|null} */
|
||||
this.__attributeCache = null;
|
||||
/** @param {SAttribute[]|null} */
|
||||
this.inheritableAttributeCache = null;
|
||||
this.__inheritableAttributeCache = null;
|
||||
|
||||
/** @param {SAttribute[]} */
|
||||
this.targetRelations = [];
|
||||
@@ -192,11 +192,11 @@ class SNote extends AbstractShacaEntity {
|
||||
}
|
||||
}
|
||||
|
||||
this.inheritableAttributeCache = [];
|
||||
this.__inheritableAttributeCache = [];
|
||||
|
||||
for (const attr of this.__attributeCache) {
|
||||
if (attr.isInheritable) {
|
||||
this.inheritableAttributeCache.push(attr);
|
||||
this.__inheritableAttributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,11 +210,11 @@ class SNote extends AbstractShacaEntity {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
|
||||
if (!this.__inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.__inheritableAttributeCache
|
||||
}
|
||||
|
||||
return this.inheritableAttributeCache;
|
||||
return this.__inheritableAttributeCache;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
|
||||
Reference in New Issue
Block a user