diff --git a/src/_h5ai/private/php/ext/class-custom.php b/src/_h5ai/private/php/ext/class-custom.php index ab1628ed2..d169ba451 100644 --- a/src/_h5ai/private/php/ext/class-custom.php +++ b/src/_h5ai/private/php/ext/class-custom.php @@ -21,11 +21,42 @@ private function read_custom_file($path, $name, &$content, &$type) { } } + /** + * Load custom options for the specified path. Traverses the directory structure to include + * options for all parent directories too. Options in children will override the options + * inherited from their ancestors. + * + * @param $href string + * @return array + */ + private function get_options(string $href) { + if (!$this->context->query_option('custom.enabled', false)) { + return []; + } + + $file_prefix = $this->context->get_setup()->get('FILE_PREFIX'); + $root_path = $this->context->get_setup()->get('ROOT_PATH'); + + // Find all options files, from the current path all the way to the root + $option_files = []; + $path = $this->context->to_path($href); + do { + $file = $path . '/' . $file_prefix . '.options.json'; + if (is_readable($file)) { + $option_files[] = Json::load($file); + } + $path = Util::normalize_path(dirname($path)); + } while ($path !== $root_path && $path !== '/' && $href !== '/'); + + return count($option_files) === 0 ? [] : array_merge(...array_reverse($option_files)); + } + public function get_customizations($href) { if (!$this->context->query_option('custom.enabled', false)) { return [ 'header' => ['content' => null, 'type' => null], - 'footer' => ['content' => null, 'type' => null] + 'footer' => ['content' => null, 'type' => null], + 'options' => (object)[], ]; } @@ -59,7 +90,8 @@ public function get_customizations($href) { return [ 'header' => ['content' => $header, 'type' => $header_type], - 'footer' => ['content' => $footer, 'type' => $footer_type] + 'footer' => ['content' => $footer, 'type' => $footer_type], + 'options' => (object)self::get_options($href), ]; } } diff --git a/src/_h5ai/public/js/lib/ext/custom.js b/src/_h5ai/public/js/lib/ext/custom.js index 2e86fba6c..cf048af76 100644 --- a/src/_h5ai/public/js/lib/ext/custom.js +++ b/src/_h5ai/public/js/lib/ext/custom.js @@ -27,6 +27,7 @@ const onLocationChanged = item => { server.request({action: 'get', custom: item.absHref}).then(response => { const data = response && response.custom; each(['header', 'footer'], key => update(data, key)); + event.pub('custom.optionsLoaded', data.options); }); }; diff --git a/src/_h5ai/public/js/lib/view/view.js b/src/_h5ai/public/js/lib/view/view.js index aff35efb0..47f06f959 100644 --- a/src/_h5ai/public/js/lib/view/view.js +++ b/src/_h5ai/public/js/lib/view/view.js @@ -18,6 +18,7 @@ const settings = Object.assign({ setParentFolderLabels: false, sizes }, allsettings.view); +let folderSettings = null; const sortedSizes = settings.sizes.sort((a, b) => a - b); const checkedModes = intersection(settings.modes, modes); const storekey = 'view'; @@ -86,15 +87,32 @@ const addCssStyles = () => { dom('').text(styles.join('\n')).appTo('head'); }; -const set = (mode, size) => { - const stored = store.get(storekey); +// Gets the first truthy value in `candidates` that is also listed in `validOptions` +const findFirstValid = (candidates, validOptions) => candidates.find(x => x && includes(validOptions, x)); - mode = mode || stored && stored.mode; - size = size || stored && stored.size; - mode = includes(settings.modes, mode) ? mode : settings.modes[0]; - size = includes(settings.sizes, size) ? size : settings.sizes[0]; - store.put(storekey, {mode, size}); +const getModes = () => checkedModes; +const getMode = () => findFirstValid( + [ + (store.get(storekey) && store.get(storekey).mode), + (folderSettings && folderSettings.mode), + settings.modes[0] + ], + settings.modes +); +const getSizes = () => sortedSizes; +const getSize = () => findFirstValid( + [ + (store.get(storekey) && store.get(storekey).size), + (folderSettings && folderSettings.size), + settings.sizes[0] + ], + settings.sizes +); + +const updateView = () => { + const mode = getMode(); + const size = getSize(); each(checkedModes, m => { if (m === mode) { $view.addCls('view-' + m); @@ -114,12 +132,25 @@ const set = (mode, size) => { event.pub('view.mode.changed', mode, size); }; -const getModes = () => checkedModes; -const getMode = () => store.get(storekey).mode; -const setMode = mode => set(mode, null); +const set = (mode, size) => { + const stored = store.get(storekey); -const getSizes = () => sortedSizes; -const getSize = () => store.get(storekey).size; + mode = mode || stored && stored.mode; + size = size || stored && stored.size; + + // Unset if it's being set back to the default value + if (mode === settings.modes[0]) { + mode = undefined; + } + if (size === settings.sizes[0]) { + size = undefined; + } + + store.put(storekey, {mode, size}); + updateView(); +}; + +const setMode = mode => set(mode, null); const setSize = size => set(null, size); const onMouseenter = ev => { @@ -258,9 +289,14 @@ const onResize = () => { } }; +const onCustomOptionsLoaded = options => { + folderSettings = options && options.view; + updateView(); +}; + const init = () => { addCssStyles(); - set(); + updateView(); $view.appTo(base.$content); $hint.hide(); @@ -270,6 +306,7 @@ const init = () => { event.sub('location.changed', onLocationChanged); event.sub('location.refreshed', onLocationRefreshed); event.sub('resize', onResize); + event.sub('custom.optionsLoaded', onCustomOptionsLoaded); onResize(); };