diff --git a/backend/models/pages.ts b/backend/models/pages.ts index 15453098e..38ff8d36e 100644 --- a/backend/models/pages.ts +++ b/backend/models/pages.ts @@ -350,8 +350,10 @@ class Pages { * Flatten a row and its blobs into the shape the API returns. * * @param locked Withhold the body — the source, the rendered HTML, the table of contents drawn from - * it, and the relation links written onto the page. The metadata stays: a reader - * looking at the lock screen is told what page they are being asked for a password to. + * it, the relation links written onto the page, and the page's own CSS and scripts, + * which the page view runs against content this reader has not been given. The + * metadata stays: a reader looking at the lock screen is told what page they are + * being asked for a password to. * @param withPassword Include the page's own password. Only for a requester who may edit the page, * which is the one that has to be able to read it back and save it again. * @param withContent Include the source. A redirection's comes back either way: its content is not @@ -405,9 +407,9 @@ class Pages { showTags: config.showTags ?? true, showToc: config.showToc ?? true, tocDepth: config.tocDepth ?? { min: 1, max: 2 }, - scriptJsLoad: scripts.jsLoad ?? '', - scriptJsUnload: scripts.jsUnload ?? '', - scriptCss: scripts.css ?? '', + scriptJsLoad: locked ? '' : (scripts.jsLoad ?? ''), + scriptJsUnload: locked ? '' : (scripts.jsUnload ?? ''), + scriptCss: locked ? '' : (scripts.css ?? ''), navigationId: row.navigationId ?? null, navigationMode: row.navigationMode ?? 'inherit', authorId: row.authorId, diff --git a/frontend/src/components/PagePropertiesDialog.vue b/frontend/src/components/PagePropertiesDialog.vue index a47a16859..c8377d0c5 100644 --- a/frontend/src/components/PagePropertiesDialog.vue +++ b/frontend/src/components/PagePropertiesDialog.vue @@ -166,10 +166,16 @@ {{ t('editor.props.localeRelationsHint') }} - + +
{{ t('editor.props.scripts') }}
{{ t('editor.props.jsUnloadHint') }} + userStore.pagePermissions.includes('write:scripts')) +const mayWriteStyles = computed(() => userStore.pagePermissions.includes('write:styles')) + +/* + The rail of jump links down the side of the panel. A computed rather than a constant because the + Scripts section is not always there, and a link to a section that is not rendered is a link that + throws -- `jumpToSection` reads the element straight off the document. +*/ +const quickaccess = computed(() => + [ + { key: 'refCardInfo', icon: 'la:info-circle', label: t('editor.props.info') }, + { key: 'refCardPublishState', icon: 'la:power-off', label: t('editor.props.publishState') }, + { key: 'refCardRelations', icon: 'la:sun', label: t('editor.props.relations') }, + { + key: 'refCardScripts', + icon: 'la:code', + label: t('editor.props.scripts'), + shown: mayWriteScripts.value || mayWriteStyles.value + }, + { key: 'refCardSidebar', icon: 'la:ruler-vertical', label: t('editor.props.sidebar') }, + { key: 'refCardSocial', icon: 'la:comments', label: t('editor.props.social') }, + { key: 'refCardTags', icon: 'la:tags', label: t('editor.props.tags') }, + { key: 'refCardVisibility', icon: 'la:eye', label: t('editor.props.visibility') } + ].filter((qa) => qa.shown !== false) +) + const publishingRange = computed({ get() { return { diff --git a/frontend/src/components/PageScriptsDialog.vue b/frontend/src/components/PageScriptsDialog.vue index 60d5685c9..1d410c18f 100644 --- a/frontend/src/components/PageScriptsDialog.vue +++ b/frontend/src/components/PageScriptsDialog.vue @@ -52,9 +52,10 @@ import UtilCodeEditor from './UtilCodeEditor.vue' // PROPS const props = defineProps({ + /** Which of the three the dialog is editing: `jsLoad`, `jsUnload` or `styles`. */ mode: { type: String, - default: 'css' + default: 'styles' } }) @@ -102,15 +103,28 @@ const languageLabel = computed(() => { } }) -const contentStoreKey = computed(() => { - return 'script' + props.mode.charAt(0).toUpperCase() + props.mode.slice(1) -}) +/* + Which store field this dialog is editing. A table rather than a name built from `mode`, because the + two do not line up: the CSS mode is called `styles` -- it is the button in the properties panel and + the translation key of its label -- while the field it writes is `scriptCss`. Assembled, it spelled + `scriptStyles`, a field the store does not have, so the CSS editor opened empty on a page that had + CSS and saved into nothing. +*/ +const STORE_KEYS = { + jsLoad: 'scriptJsLoad', + jsUnload: 'scriptJsUnload', + styles: 'scriptCss' +} + +const contentStoreKey = computed(() => STORE_KEYS[props.mode]) // METHODS function persist() { + // -> `.value`: the computed itself as a key stringifies to `[object Object]`, which is where every + // edit made in this dialog used to go pageStore.$patch({ - [contentStoreKey]: state.content + [contentStoreKey.value]: state.content }) } @@ -129,7 +143,7 @@ function saveAndClose() { // -> No deferred mount: the quarter-second wait was there to give the old editor a laid-out container // to measure itself against, and a textarea needs no such thing onMounted(() => { - state.content = pageStore[contentStoreKey.value] + state.content = pageStore[contentStoreKey.value] ?? '' // -> The editor is what this dialog is for, so the caret starts there. After the tick that renders // the content above, so focus lands on a field that is already populated. nextTick(() => { diff --git a/frontend/src/components/shared/WToggle.vue b/frontend/src/components/shared/WToggle.vue index 55023ca94..071aa79ca 100644 --- a/frontend/src/components/shared/WToggle.vue +++ b/frontend/src/components/shared/WToggle.vue @@ -21,8 +21,16 @@ :class="[dense ? 'size-4' : 'size-5', knobOffset]" /> + +