├── individual.png ├── resources ├── lang │ ├── ca.mo │ ├── cs.mo │ ├── da.mo │ ├── de.mo │ ├── es.mo │ ├── fr.mo │ ├── nl.mo │ ├── nn.mo │ ├── ru.mo │ ├── sk.mo │ ├── sv.mo │ ├── ta.mo │ ├── tr.mo │ ├── uk.mo │ ├── vi.mo │ └── nb_NO.mo └── views │ ├── icons │ └── config-gedcom-field.phtml │ ├── selects │ ├── media.phtml │ └── individual.phtml │ ├── individual-page-names.phtml │ ├── edit │ ├── icon-config-gedcom-field.phtml │ ├── edit-gedcom-fields-switch.phtml │ ├── config-gedcom-field-edit-control.phtml │ ├── config-gedcom-field-edit-control-2.phtml │ ├── existing-record.phtml │ ├── edit-gedcom-fields-ext2.phtml │ └── edit-gedcom-fields-ext.phtml │ ├── admin │ ├── tags-ext.phtml │ └── badges │ │ ├── config.phtml │ │ └── edit.phtml │ ├── individual-page-sidebars.phtml │ ├── individual-page-title.phtml │ ├── modals │ └── config-gedcom-field.phtml │ ├── individual-page.phtml │ ├── individual-page-images.phtml │ ├── family-page-menu.phtml │ ├── individual-name.phtml │ ├── layouts │ ├── defaultJustLight.phtml │ └── default.phtml │ ├── individual-page-menu.phtml │ ├── chart-box.phtml │ └── css │ └── theme.phtml ├── individual_compact.png ├── WhatsNew ├── WhatsNew1.php ├── WhatsNew2.php ├── WhatsNew4.php ├── WhatsNew0.php └── WhatsNew3.php ├── patchedWebtrees ├── CommonMark │ ├── CustomCommonMarkConverter.php │ ├── CustomCensusTableExtension.php │ └── CustomCensusTableParser.php ├── Http │ └── RequestHandlers │ │ ├── EditGedcomFieldsArgs.php │ │ ├── AddUnlinkedPageExt.php │ │ ├── AddChildToFamilyPageExt.php │ │ ├── AddSpouseToFamilyPageExt.php │ │ ├── AddChildToIndividualPageExt.php │ │ ├── AddParentToIndividualPageExt.php │ │ ├── AddSpouseToIndividualPageExt.php │ │ ├── ConfigGedcomField.php │ │ ├── ConfigGedcomFieldAction.php │ │ ├── AddNewFactExt.php │ │ ├── EditFactPageExt.php │ │ ├── EditMainFieldsPage.php │ │ └── EditMainFieldsAction.php ├── FamilyNameHandler.php ├── Elements │ ├── Level1SubmitterText.php │ └── Level1NoteStructure.php ├── FamilyExt.php ├── IndividualExtSettings.php ├── CustomIndividualFactory.php ├── IndividualNameHandler.php ├── CustomFamilyFactory.php ├── CustomMarkdownFactory.php ├── GedcomRecordPageTempReplacement.php ├── IndividualExt.php └── Services │ └── GedcomEditServiceExt2.php ├── po.bat ├── autoload.php ├── IvoPetkov ├── LICENSE ├── HTML5DOMNodeList.php ├── HTML5DOMTokenList.php └── HTML5DOMElement.php ├── Factories └── CustomXrefFactory.php ├── SurnameTradition └── SurnameTraditionWrapper.php ├── module.php ├── PlaceholderModule.php ├── README.md └── metadata.json /individual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/individual.png -------------------------------------------------------------------------------- /resources/lang/ca.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/ca.mo -------------------------------------------------------------------------------- /resources/lang/cs.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/cs.mo -------------------------------------------------------------------------------- /resources/lang/da.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/da.mo -------------------------------------------------------------------------------- /resources/lang/de.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/de.mo -------------------------------------------------------------------------------- /resources/lang/es.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/es.mo -------------------------------------------------------------------------------- /resources/lang/fr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/fr.mo -------------------------------------------------------------------------------- /resources/lang/nl.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/nl.mo -------------------------------------------------------------------------------- /resources/lang/nn.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/nn.mo -------------------------------------------------------------------------------- /resources/lang/ru.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/ru.mo -------------------------------------------------------------------------------- /resources/lang/sk.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/sk.mo -------------------------------------------------------------------------------- /resources/lang/sv.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/sv.mo -------------------------------------------------------------------------------- /resources/lang/ta.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/ta.mo -------------------------------------------------------------------------------- /resources/lang/tr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/tr.mo -------------------------------------------------------------------------------- /resources/lang/uk.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/uk.mo -------------------------------------------------------------------------------- /resources/lang/vi.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/vi.mo -------------------------------------------------------------------------------- /individual_compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/individual_compact.png -------------------------------------------------------------------------------- /resources/lang/nb_NO.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vesta-webtrees-2-custom-modules/vesta_classic_laf/HEAD/resources/lang/nb_NO.mo -------------------------------------------------------------------------------- /resources/views/icons/config-gedcom-field.phtml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /resources/views/selects/media.phtml: -------------------------------------------------------------------------------- 1 | mediaFiles() as $media_file) : ?> 2 | 5 | displayImage(30, 40, 'contain', []) ?> 6 | 7 | fullName(); 8 | -------------------------------------------------------------------------------- /resources/views/selects/individual.phtml: -------------------------------------------------------------------------------- 1 | 6 | 7 | findHighlightedMediaFile() instanceof MediaFile) : ?> 8 | 11 | findHighlightedMediaFile()->displayImage(30, 40, 'contain', []) ?> 12 | 13 | fullName() ?>, lifespan() ?> 14 | -------------------------------------------------------------------------------- /WhatsNew/WhatsNew1.php: -------------------------------------------------------------------------------- 1 | canConfigure; 12 | } 13 | 14 | public function __construct( 15 | bool $canConfigure = false) { 16 | 17 | $this->canConfigure = $canConfigure; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /WhatsNew/WhatsNew3.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 |
12 | facts(['NAME']) as $fact) : ?> 13 | $fact]) ?> 14 | 15 | 16 | 19 |
20 | -------------------------------------------------------------------------------- /patchedWebtrees/FamilyNameHandler.php: -------------------------------------------------------------------------------- 1 | appendXref = $appendXref; 11 | } 12 | 13 | public function addXref(string $nameForDisplay, string $xref): string { 14 | if (!$this->appendXref) { 15 | return $nameForDisplay; 16 | } 17 | return $nameForDisplay . ' (' . $xref . ')'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/AddUnlinkedPageExt.php: -------------------------------------------------------------------------------- 1 | ' . e($value) . ''; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/AddChildToIndividualPageExt.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | -------------------------------------------------------------------------------- /patchedWebtrees/FamilyExt.php: -------------------------------------------------------------------------------- 1 | addXref($full, $this->xref()); 26 | return $full; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | addPsr4('Cissee\\Webtrees\\Module\\ClassicLAF\\', __DIR__); 7 | $loader->addPsr4('Cissee\\Webtrees\\Module\\ClassicLAF\\Factories\\', __DIR__ . "/Factories"); 8 | $loader->addPsr4('Cissee\\Webtrees\\Module\\ClassicLAF\\SurnameTradition\\', __DIR__ . "/SurnameTradition"); 9 | $loader->addPsr4('Cissee\\WebtreesExt\\', __DIR__ . "/patchedWebtrees"); 10 | $loader->addPsr4('Cissee\\WebtreesExt\\CommonMark\\', __DIR__ . "/patchedWebtrees/CommonMark"); 11 | $loader->addPsr4('Cissee\\WebtreesExt\\Http\\RequestHandlers\\', __DIR__ . "/patchedWebtrees/Http/RequestHandlers"); 12 | 13 | $loader->addPsr4('IvoPetkov\\', __DIR__ . "/IvoPetkov"); 14 | $loader->addPsr4('IvoPetkov\HTML5DOMDocument\Internal\\', __DIR__ . "/IvoPetkov/HTML5DOMDocument/Internal"); 15 | 16 | $loader->register(); 17 | -------------------------------------------------------------------------------- /patchedWebtrees/IndividualExtSettings.php: -------------------------------------------------------------------------------- 1 | compactIndividualPage; 13 | } 14 | 15 | public function cropThumbnails(): bool { 16 | return $this->cropThumbnails; 17 | } 18 | 19 | public function expandFirstSidebar(): bool { 20 | return $this->expandFirstSidebar; 21 | } 22 | 23 | public function __construct( 24 | bool $compactIndividualPage, 25 | bool $cropThumbnails, 26 | bool $expandFirstSidebar) 27 | { 28 | $this->compactIndividualPage = $compactIndividualPage; 29 | $this->cropThumbnails = $cropThumbnails; 30 | $this->expandFirstSidebar = $expandFirstSidebar; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /resources/views/edit/edit-gedcom-fields-switch.phtml: -------------------------------------------------------------------------------- 1 | $hierarchy 11 | * @var string $prefix 12 | * @var Tree $tree 13 | */ 14 | 15 | $args = \Vesta\VestaUtils::get(EditGedcomFieldsArgs::class); 16 | 17 | if($args->canConfigure()) { 18 | //own view, plus extension for 'expand subtags' 19 | echo view('edit/edit-gedcom-fields-ext', [ 20 | 'gedcom' => $gedcom, 21 | 'hierarchy' => $hierarchy, 22 | 'prefix' => $prefix, 23 | 'tree' => $tree]); 24 | 25 | return; 26 | } 27 | 28 | //original view, plus extension for 'expand subtags' 29 | echo view('edit/edit-gedcom-fields-ext2', [ 30 | 'gedcom' => $gedcom, 31 | 'hierarchy' => $hierarchy, 32 | 'prefix' => $prefix, 33 | 'tree' => $tree]); 34 | -------------------------------------------------------------------------------- /resources/views/edit/config-gedcom-field-edit-control.phtml: -------------------------------------------------------------------------------- 1 | getPreference($tree, $tag); 8 | 9 | $options = []; 10 | $options[''] = I18N::translate('no adjustment (use setting for all events as shown below)'); 11 | $options['LEVEL0'] = I18N::translate('hide always'); 12 | $options['LEVEL1'] = I18N::translate('hide in multi-fact dialogs, show otherwise'); 13 | if ($indent) { 14 | $options['LEVEL1a'] = I18N::translate('hide in multi-fact dialogs, show and expand subtags otherwise'); 15 | } 16 | $options['LEVEL2'] = I18N::translate('show always'); 17 | if ($indent) { 18 | $options['LEVEL2a'] = I18N::translate('show and expand subtags always'); 19 | } 20 | 21 | ?> 22 | 23 |
24 | 'CONFIG_GEDCOM_FIELDS', 26 | 'options' => $options, 27 | 'selected' => $override]) ?> 28 |
29 | -------------------------------------------------------------------------------- /IvoPetkov/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Ivo Petkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/ConfigGedcomField.php: -------------------------------------------------------------------------------- 1 | tree(); 21 | $tag = Validator::attributes($request)->string('tag'); 22 | $indent = Validator::attributes($request)->boolean('indent'); 23 | $tag2parts = explode(':',$tag); 24 | $tag2parts[1] = '*'; 25 | $tag2 = implode(':',$tag2parts); 26 | 27 | $html = view('modals/config-gedcom-field', [ 28 | 'tree' => $tree, 29 | 'tag' => $tag, 30 | 'tag2' => $tag2, 31 | 'indent' => $indent, 32 | ]); 33 | 34 | return response($html); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /IvoPetkov/HTML5DOMNodeList.php: -------------------------------------------------------------------------------- 1 | offsetExists($index) ? $this->offsetGet($index) : null; 29 | } 30 | 31 | /** 32 | * Returns the value for the property specified. 33 | * 34 | * @param string $name The name of the property. 35 | * @return mixed 36 | * @throws \Exception 37 | */ 38 | public function __get(string $name) 39 | { 40 | if ($name === 'length') { 41 | return sizeof($this); 42 | } 43 | throw new \Exception('Undefined property: \IvoPetkov\HTML5DOMNodeList::$' . $name); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Factories/CustomXrefFactory.php: -------------------------------------------------------------------------------- 1 | module = $module; 15 | } 16 | 17 | /** @var string[] Which module preference is used for which record type */ 18 | static $type_to_preference = array( 19 | 'INDI' => 'GEDCOM_ID_PREFIX', 20 | 'FAM' => 'FAM_ID_PREFIX', 21 | 'OBJE' => 'MEDIA_ID_PREFIX', 22 | 'NOTE' => 'NOTE_ID_PREFIX', 23 | 'SOUR' => 'SOURCE_ID_PREFIX', 24 | 'REPO' => 'REPO_ID_PREFIX', 25 | '_LOC' => 'LOCATION_ID_PREFIX', 26 | ); 27 | 28 | public function make(string $record_type): string { 29 | //[RC] taken from webtrees 1.x and adjusted 30 | //Fallback: Use the first non-underscore character 31 | $prefix = substr(trim($record_type, '_'), 0, 1); 32 | if (($record_type === null) || ($this->module === null)) { 33 | $prefix = 'X'; 34 | } else if (array_key_exists($record_type, self::$type_to_preference)) { 35 | $prefix = $this->module->getPreference(self::$type_to_preference[$record_type], $prefix); 36 | } 37 | 38 | return $this->generate($prefix, ''); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/ConfigGedcomFieldAction.php: -------------------------------------------------------------------------------- 1 | tree(); 19 | $params = (array) $request->getParsedBody(); 20 | 21 | $tag = $params['tag']; 22 | $value = $params['CONFIG_GEDCOM_FIELDS']; 23 | 24 | $tag2parts = explode(':',$tag); 25 | $tag2parts[1] = '*'; 26 | $tag2 = implode(':',$tag2parts); 27 | $value2 = $params['CONFIG_GEDCOM_FIELDS_2']; 28 | 29 | $gedcom_edit_service = new GedcomEditServiceExt2(); 30 | $gedcom_edit_service->setPreference($tree, $tag, $value); 31 | $gedcom_edit_service->setPreference($tree, $tag2, $value2); 32 | 33 | // value and text are for autocomplete 34 | // html is for interactive modals 35 | return response(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/views/edit/config-gedcom-field-edit-control-2.phtml: -------------------------------------------------------------------------------- 1 | getPreference($tree, $tag); 8 | 9 | $options = []; 10 | $options[''] = I18N::translate('no adjustment'); 11 | $options['LEVEL0'] = I18N::translate('hide always'); 12 | $options['LEVEL1'] = I18N::translate('hide in multi-fact dialogs, show otherwise'); 13 | if ($indent) { 14 | $options['LEVEL1a'] = I18N::translate('hide in multi-fact dialogs, show and expand subtags otherwise'); 15 | } 16 | $options['LEVEL2'] = I18N::translate('show always'); 17 | if ($indent) { 18 | $options['LEVEL2a'] = I18N::translate('show and expand subtags always'); 19 | } 20 | 21 | $tag2parts = explode(':',$tag); 22 | $tagHead = array_shift($tag2parts); 23 | array_shift($tag2parts); 24 | $tagRemaining = implode(':',$tag2parts); 25 | 26 | ?> 27 | 28 |
29 | 30 | 31 | 'CONFIG_GEDCOM_FIELDS_2', 33 | 'options' => $options, 34 | 'selected' => $override]) ?> 35 |
36 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/AddNewFactExt.php: -------------------------------------------------------------------------------- 1 | getQueryParams()['include_hidden'] ?? false); 28 | 29 | $can_configure = Auth::isAdmin() && $include_hidden; 30 | 31 | if ($can_configure) { 32 | //explicitly register in order to re-use in views where we cannot pass via variable 33 | \Vesta\VestaUtils::set(EditGedcomFieldsArgs::class, new EditGedcomFieldsArgs(true)); 34 | } 35 | 36 | return parent::handle($request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/EditFactPageExt.php: -------------------------------------------------------------------------------- 1 | getQueryParams()['include_hidden'] ?? false); 28 | 29 | $can_configure = Auth::isAdmin() && $include_hidden; 30 | 31 | if ($can_configure) { 32 | //explicitly register in order to re-use in views where we cannot pass via variable 33 | \Vesta\VestaUtils::set(EditGedcomFieldsArgs::class, new EditGedcomFieldsArgs(true)); 34 | } 35 | 36 | return parent::handle($request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /patchedWebtrees/CommonMark/CustomCensusTableExtension.php: -------------------------------------------------------------------------------- 1 | addBlockParser(new CustomCensusTableParser()) 30 | ->addBlockRenderer(Table::class, new TableRenderer()) 31 | ->addBlockRenderer(TableSection::class, new TableSectionRenderer()) 32 | ->addBlockRenderer(TableRow::class, new TableRowRenderer()) 33 | ->addBlockRenderer(TableCell::class, new TableCellRenderer()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /resources/views/admin/tags-ext.phtml: -------------------------------------------------------------------------------- 1 | $all_family_tags, 10 | 'all_individual_tags' => $all_individual_tags, 11 | 'custom_family_tags' => $custom_family_tags, 12 | 'custom_gedcom_l_tags' => $custom_gedcom_l_tags, 13 | 'custom_individual_tags' => $custom_individual_tags, 14 | 'custom_fam_fact' => $custom_fam_fact, 15 | 'custom_fam_nchi' => $custom_fam_nchi, 16 | 'custom_resi_value' => $custom_resi_value, 17 | 'custom_time_tags' => $custom_time_tags, 18 | 'element_factory' => $element_factory, 19 | 'title' => $title, 20 | ]); 21 | 22 | $parentHtml = ob_get_clean(); 23 | 24 | $search = "

" . $title . "

"; 25 | 26 | $link = ''.I18N::translate('here').''; 27 | 28 | $ext = I18N::translate( 29 | 'Note that in addition to the configuration options below, %1$s allows you to configure the visibility of each specific tag in detail, as described %2$s.', 30 | CommonI18N::titleVestaCLAF(), 31 | $link); 32 | 33 | $adjustedHtml = str_replace($search, $search . $ext, $parentHtml); 34 | 35 | echo $adjustedHtml; 36 | 37 | ?> 38 | -------------------------------------------------------------------------------- /patchedWebtrees/CustomIndividualFactory.php: -------------------------------------------------------------------------------- 1 | settings = $settings; 22 | } 23 | 24 | public function make(string $xref, Tree $tree, string $gedcom = null): ?Individual 25 | { 26 | $cache = Registry::cache()->array(); 27 | 28 | return $cache->remember(__CLASS__ . $xref . '@' . $tree->id(), function () use ($xref, $tree, $gedcom) { 29 | $gedcom = $gedcom ?? $this->gedcom($xref, $tree); 30 | $pending = $this->pendingChanges($tree)->get($xref); 31 | 32 | if ($gedcom === null && ($pending === null || !preg_match(self::TYPE_CHECK_REGEX, $pending))) { 33 | return null; 34 | } 35 | $xref = $this->extractXref($gedcom ?? $pending, $xref); 36 | 37 | return new IndividualExt($xref, $gedcom ?? '', $pending, $tree, $this->settings); 38 | }); 39 | } 40 | 41 | public function new(string $xref, string $gedcom, ?string $pending, Tree $tree): Individual 42 | { 43 | return new IndividualExt($xref, $gedcom, $pending, $tree, $this->settings); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SurnameTradition/SurnameTraditionWrapper.php: -------------------------------------------------------------------------------- 1 | actual = $actual; 18 | } 19 | 20 | public function name(): string { 21 | return $this->actual->name(); 22 | } 23 | 24 | public function description(): string { 25 | return $this->actual->description(); 26 | } 27 | 28 | public function defaultName(): string { 29 | return $this->actual->defaultName(); 30 | } 31 | 32 | public function newChildNames(?Individual $father, ?Individual $mother, string $sex): array { 33 | $names = $this->actual->newChildNames($father, $mother, $sex); 34 | return $this->adjust($names); 35 | } 36 | 37 | public function newParentNames(Individual $child, string $sex): array { 38 | $names = $this->actual->newParentNames($child, $sex); 39 | return $this->adjust($names); 40 | } 41 | 42 | public function newSpouseNames(Individual $spouse, string $sex): array { 43 | $names = $this->actual->newSpouseNames($spouse, $sex); 44 | return $this->adjust($names); 45 | } 46 | 47 | protected function adjust(array $names): array { 48 | if (sizeof($names) !== 1) { 49 | return $names; 50 | } 51 | 52 | //remove NameType::VALUE_BIRTH if there is only one name! 53 | $name = $names[0]; 54 | $name = str_replace("\n2 TYPE BIRTH", "", $name); 55 | return [$name]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /resources/views/individual-page-sidebars.phtml: -------------------------------------------------------------------------------- 1 | $sidebars 9 | */ 10 | 11 | $expandFirstSidebar = $record->settings()->expandFirstSidebar(); 12 | ?> 13 | 14 | 42 | -------------------------------------------------------------------------------- /patchedWebtrees/IndividualNameHandler.php: -------------------------------------------------------------------------------- 1 | nickBeforeSurn = $nickBeforeSurn; 15 | } 16 | 17 | public function setAppendXref(bool $appendXref) { 18 | $this->appendXref = $appendXref; 19 | } 20 | 21 | public function setAddBadgesCallback($addBadgesCallback) { 22 | $this->addBadgesCallback = $addBadgesCallback; 23 | } 24 | 25 | public function addNick(string $nameForDisplay, string $nick): string { 26 | if ($this->nickBeforeSurn) { 27 | //same logic as in webtrees 1.x 28 | $pos = strpos($nameForDisplay, '/'); 29 | if ($pos === false) { 30 | // No surname - just append it 31 | return $nameForDisplay . ' "' . $nick . '"'; 32 | } else { 33 | // Insert before surname 34 | return substr($nameForDisplay, 0, $pos) . '"' . $nick . '" ' . substr($nameForDisplay, $pos); 35 | } 36 | } else { 37 | //same logic as in original webtrees 2.x, which has now changed to: 'don't display at all!' 38 | return $nameForDisplay; 39 | } 40 | } 41 | 42 | public function addXref(string $nameForDisplay, string $xref): string { 43 | if (!$this->appendXref or ('xref' == $xref)) { 44 | //'xref' indicates fake record, cf individual-name.phtml 45 | return $nameForDisplay; 46 | } 47 | return $nameForDisplay . ' (' . $xref . ')'; 48 | } 49 | 50 | public function addBadges(string $name, Tree $tree, string $gedcom): string { 51 | 52 | return ($this->addBadgesCallback)($name, $tree, $gedcom); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /module.php: -------------------------------------------------------------------------------- 1 | filter(static function (string $filename): bool { 27 | // Special characters will break PHP variable names. 28 | // This also allows us to ignore modules called "foo.example" and "foo.disable" 29 | $module_name = basename(dirname($filename)); 30 | 31 | foreach (['.', ' ', '[', ']'] as $character) { 32 | if (str_contains($module_name, $character)) { 33 | return false; 34 | } 35 | } 36 | 37 | return strlen($module_name) <= 30; 38 | }) 39 | ->each(static function (string $filename): void { 40 | require_once $filename; 41 | }); 42 | 43 | //dependency check 44 | $ok = class_exists("Cissee\WebtreesExt\AbstractModule", true); 45 | if (!$ok) { 46 | FlashMessages::addMessage("Missing dependency - Make sure to install all Vesta modules!"); 47 | return; 48 | } 49 | 50 | $placeholder = \Vesta\VestaUtils::get(PlaceholderModule::class); 51 | return $placeholder->ifIncompatible() ?? \Vesta\VestaUtils::get(ClassicLAFModule::class); 52 | -------------------------------------------------------------------------------- /patchedWebtrees/CustomFamilyFactory.php: -------------------------------------------------------------------------------- 1 | array()->remember(__CLASS__ . $xref . '@' . $tree->id(), function () use ($xref, $tree, $gedcom) { 23 | $gedcom = $gedcom ?? $this->gedcom($xref, $tree); 24 | $pending = $this->pendingChanges($tree)->get($xref); 25 | 26 | if ($gedcom === null && ($pending === null || !preg_match(self::TYPE_CHECK_REGEX, $pending))) { 27 | return null; 28 | } 29 | 30 | $xref = $this->extractXref($gedcom ?? $pending, $xref); 31 | 32 | //https://www.webtrees.net/index.php/forum/help-for-release-2-2-x/40264-stack-overflow-solved 33 | //note: removing this alone doesn't help wrt stack overflow/ memoty exhaustion 34 | //e.g. in descendany report in case of loops 35 | // Preload all the family members using a single database query. 36 | preg_match_all('/\n1 (?:HUSB|WIFE|CHIL) @(' . Gedcom::REGEX_XREF . ')@/', $gedcom . "\n" . $pending, $match); 37 | DB::table('individuals') 38 | ->where('i_file', '=', $tree->id()) 39 | ->whereIn('i_id', $match[1]) 40 | ->get() 41 | ->map(Registry::individualFactory()->mapper($tree)); 42 | 43 | return new FamilyExt($xref, $gedcom ?? '', $pending, $tree); 44 | }); 45 | } 46 | 47 | public function new(string $xref, string $gedcom, ?string $pending, Tree $tree): Family 48 | { 49 | return new FamilyExt($xref, $gedcom, $pending, $tree); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/views/individual-page-title.phtml: -------------------------------------------------------------------------------- 1 | $users 13 | */ 14 | 15 | ?> 16 | 17 | fullName() ?> 18 | facts(['SEX']) as $n => $fact) { 21 | $record = $fact->record(); 22 | 23 | switch ($fact->value()) { 24 | case 'M': 25 | $sex = MoreI18N::xlate('Male'); 26 | $container_class = 'male_gender'; 27 | $icon = view('icons/sex', ['sex' => 'M']); 28 | break; 29 | case 'F': 30 | $sex = MoreI18N::xlate('Female'); 31 | $container_class = 'female_gender'; 32 | $icon = view('icons/sex', ['sex' => 'F']); 33 | break; 34 | default: 35 | $sex = MoreI18N::xlate('unknown gender', 'Unknown'); 36 | $container_class = 'unknown_gender'; 37 | $icon = view('icons/sex', ['sex' => 'U']); 38 | break; 39 | } 40 | 41 | //$container_class = 'card'; 42 | if ($fact->isPendingDeletion()) { 43 | $container_class .= ' wt-old'; 44 | } elseif ($fact->isPendingAddition()) { 45 | $container_class .= ' wt-new'; 46 | } 47 | 48 | if ($record->canEdit()) { 49 | $edit_links = '' . view('icons/edit') . '' . MoreI18N::xlate('Edit the sex') . ''; 50 | } else { 51 | $edit_links = ''; 52 | } 53 | 54 | echo ''.$icon.$edit_links.', '; 55 | } 56 | ?> 57 | lifespan() ?> 58 | 59 | 60 | — 61 | userName()) ?> 62 | 63 | 64 | -------------------------------------------------------------------------------- /resources/views/modals/config-gedcom-field.phtml: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | 18 | $title]) ?> 19 | 20 | 26 | 27 | $title2]) ?> 28 | 29 | 35 | 36 | 39 | 40 | 41 | 42 |
43 | 44 | 62 | -------------------------------------------------------------------------------- /resources/views/individual-page.phtml: -------------------------------------------------------------------------------- 1 | $clipboard_facts 11 | * @var Collection $individual_media 12 | * @var Individual $record 13 | * @var Collection $shares 14 | * @var Collection $sidebars 15 | * @var Collection $tabs 16 | * @var Tree $tree 17 | * @var Collection $users 18 | */ 19 | ?> 20 | 21 | $record]) ?> 22 | 23 |
24 |

25 | $age, 'record' => $record, 'users' => $users]) ?> 26 |

27 | 28 | canEdit()) : ?> 29 | $can_upload_media, 'clipboard_facts' => $clipboard_facts, 'record' => $record, 'shares' => $shares]) ?> 30 | 31 |
32 | 33 | isEmpty()) : ?> 34 |
35 | $can_upload_media, 'individual_media' => $individual_media, 'record' => $record, 'tree' => $tree]) ?> 36 | 37 | $record]) ?> 38 |
39 | 40 | $record, 'tabs' => $tabs]) ?> 41 | 42 |
43 | 47 |
48 |
49 | $can_upload_media, 'individual_media' => $individual_media, 'record' => $record, 'tree' => $tree]) ?> 50 | 51 | $record]) ?> 52 |
53 | 54 | $record, 'tabs' => $tabs]) ?> 55 |
56 | 57 | 61 |
62 | $record, 'sidebars' => $sidebars]) ?> 63 |
64 |
65 | 66 | 67 | 68 | $shares, 'title' => I18N::translate('Share') . ' — ' . $record->fullName()]) ?> 69 | -------------------------------------------------------------------------------- /patchedWebtrees/Elements/Level1NoteStructure.php: -------------------------------------------------------------------------------- 1 | edit($id, $name, $value, $tree); 36 | } 37 | 38 | // Existing inline note. 39 | if ($value !== '') { 40 | return $submitter_text->edit($id, $name, $value, $tree); 41 | } 42 | 43 | $options = [ 44 | 'inline' => MoreI18N::xlate('inline note'), 45 | 'shared' => MoreI18N::xlate('shared note'), 46 | ]; 47 | 48 | // New note - either inline or shared 49 | return 50 | '
' . 51 | '
' . 52 | view('components/radios-inline', ['name' => $id . '-options', 'options' => $options, 'selected' => 'inline']) . 53 | '
' . 54 | '
' . 55 | $submitter_text->edit($id, $name, $value, $tree) . 56 | '
' . 57 | '
' . 58 | $xref_note->edit($id . '-select', $name, $value, $tree) . 59 | '
' . 60 | '
' . 61 | ''; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /patchedWebtrees/CustomMarkdownFactory.php: -------------------------------------------------------------------------------- 1 | false, 32 | 'html_input' => EnvironmentInterface::HTML_INPUT_ESCAPE, 33 | ]; 34 | 35 | /** 36 | * @param Tree|null $tree 37 | * 38 | * @return CommonMarkConverter 39 | */ 40 | public function autolink(string $markdown, Tree $tree = null): string 41 | { 42 | // Create a minimal commonmark processor - just add support for auto-links. 43 | $environment = new Environment(); 44 | $environment->addBlockRenderer(Document::class, new DocumentRenderer()); 45 | $environment->addBlockRenderer(Paragraph::class, new ParagraphRenderer()); 46 | $environment->addInlineRenderer(Text::class, new TextRenderer()); 47 | $environment->addInlineRenderer(Link::class, new LinkRenderer()); 48 | $environment->addExtension(new AutolinkExtension()); 49 | 50 | // Optionally create links to other records. 51 | if ($tree instanceof Tree) { 52 | $environment->addExtension(new XrefExtension($tree)); 53 | } 54 | 55 | $converter = new MarkDownConverter($environment); 56 | 57 | return $converter->convert($markdown)->getContent(); 58 | } 59 | 60 | /** 61 | * @param Tree|null $tree 62 | * 63 | * @return CommonMarkConverter 64 | */ 65 | public function markdown(string $markdown, Tree $tree = null): string 66 | { 67 | $environment = Environment::createCommonMarkEnvironment(); 68 | 69 | // Wrap tables so support horizontal scrolling with bootstrap. 70 | $environment->addExtension(new ResponsiveTableExtension()); 71 | 72 | // Convert webtrees 1.x style census tables to commonmark format. 73 | //[PATCHED] 74 | $environment->addExtension(new CustomCensusTableExtension()); 75 | 76 | // Optionally create links to other records. 77 | if ($tree instanceof Tree) { 78 | $environment->addExtension(new XrefExtension($tree)); 79 | } 80 | 81 | $converter = new MarkDownConverter($environment); 82 | 83 | return $converter->convert($markdown)->getContent(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /resources/views/edit/existing-record.phtml: -------------------------------------------------------------------------------- 1 | 26 | 27 |

28 | 29 |
30 | $prefix_facts) : ?> 31 | $fact) : ?> 32 |
33 |
34 | 38 | label() ?> 39 |
40 |
41 | $gedcom_edit_service->insertMissingFactSubtags($fact, false), 'hierarchy' => explode(':', $fact->tag()), 'tree' => $fact->record()->tree(), 'prefix' => $prefix]) ?> 42 |
43 |
44 | 45 | 46 | 47 |
48 |
49 | 54 | 55 | 56 | 58 | 59 |
60 |
61 | 62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | map(function (GovIdEditControlsInterface $module) { 73 | return $module->govIdEditControlSelectScriptSnippet(); 74 | }) 75 | ->toArray(); 76 | 77 | echo view(VestaUtils::vestaViewsNamespace() . '::modals/ajax-modal-vesta', [ 78 | 'ajax' => false, 79 | 'select2Initializers' => $select2Initializers 80 | ]); 81 | ?> 82 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/EditMainFieldsPage.php: -------------------------------------------------------------------------------- 1 | gedcom_edit_service = new GedcomEditServiceExt2(true); 28 | 29 | //explicitly register in order to re-use in views where we cannot pass via variable 30 | \Vesta\VestaUtils::set(GedcomEditServiceExt2::class, new GedcomEditServiceExt2(true)); 31 | } 32 | 33 | public function handle(ServerRequestInterface $request): ResponseInterface 34 | { 35 | $tree = Validator::attributes($request)->tree(); 36 | $xref = Validator::attributes($request)->isXref()->string('xref'); 37 | $record = Registry::gedcomRecordFactory()->make($xref, $tree); 38 | $record = Auth::checkRecordAccess($record, true); 39 | 40 | $names = array(); 41 | $newFacts = array(); 42 | $substrLength = 0; 43 | if ($record instanceof Individual) { 44 | $individual = $record; 45 | $sex = $individual->sex(); 46 | $newFacts = $this->gedcom_edit_service->newIndividualFacts($tree, $sex, $names); 47 | //skip merging existingNames for now 48 | 49 | //strip off 'INDI:' 50 | $substrLength = 5; 51 | } else if ($record instanceof Family) { 52 | $family = $record; 53 | $newFacts = $this->gedcom_edit_service->newFamilyFacts($tree); 54 | 55 | //strip off 'FAM:' 56 | $substrLength = 4; 57 | } 58 | 59 | $facts = array(); 60 | 61 | $counter = 1; 62 | foreach ($newFacts as $newFact) { 63 | $tag = substr($newFact->tag(),$substrLength); 64 | //error_log("tag:".$tag); 65 | $existingFacts = $record->facts([$tag]); 66 | //error_log(print_r($existingFacts, true)); 67 | 68 | if (count($existingFacts) > 0) { 69 | foreach ($existingFacts as $existingFact) { 70 | $facts['fact-'.$existingFact->id().'-'] = [$existingFact]; 71 | } 72 | } else { 73 | $facts['new-'.$counter++.'-'] = [$newFact]; 74 | } 75 | } 76 | 77 | return $this->viewResponse('edit/existing-record', [ 78 | 'facts' => $facts, 79 | 'gedcom_edit_service' => $this->gedcom_edit_service, 80 | 'post_url' => route(EditMainFieldsAction::class, ['tree' => $tree->name(), 'xref' => $xref]), 81 | 'title' => $record->fullName(), 82 | 'tree' => $tree, 83 | 'url' => Validator::queryParams($request)->isLocalUrl()->string('url', $record->url()), 84 | ]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /patchedWebtrees/Http/RequestHandlers/EditMainFieldsAction.php: -------------------------------------------------------------------------------- 1 | gedcom_edit_service = $gedcom_edit_service; 24 | } 25 | 26 | public function handle(ServerRequestInterface $request): ResponseInterface 27 | { 28 | $tree = Validator::attributes($request)->tree(); 29 | $xref = Validator::attributes($request)->isXref()->string('xref'); 30 | $record = Registry::gedcomRecordFactory()->make($xref, $tree); 31 | $record = Auth::checkRecordAccess($record, true); 32 | 33 | $keep_chan = Validator::parsedBody($request)->boolean('keep_chan', false); 34 | 35 | $fact_ids = array(); 36 | $new_ids = array(); 37 | 38 | $keys = array_keys((array)$request->getParsedBody()); 39 | foreach ($keys as $key) { 40 | //error_log("?".$key); 41 | $parts = explode("-",$key); 42 | 43 | if (count($parts) === 3) { 44 | if ($parts[0] === 'fact') { 45 | //fact-f65dc294a5d862a94b6a891b07db3d5f-levels 46 | $fact_ids[$parts[1]] = $parts[1]; 47 | } else if ($parts[0] === 'new') { 48 | //new-1-levels 49 | $new_ids[$parts[1]] = $parts[1]; 50 | } 51 | } 52 | } 53 | 54 | //error_log(print_r($keys, true)); 55 | //error_log(print_r($fact_ids, true)); 56 | 57 | //existing facts 58 | foreach ($fact_ids as $fact_id) { 59 | $levels = Validator::parsedBody($request)->array('fact-'.$fact_id.'-levels'); 60 | $tags = Validator::parsedBody($request)->array('fact-'.$fact_id.'-tags'); 61 | $values = Validator::parsedBody($request)->array('fact-'.$fact_id.'-values'); 62 | $gedcom = $this->gedcom_edit_service->editLinesToGedcom(Individual::RECORD_TYPE, $levels, $tags, $values); 63 | 64 | // Update (only the first copy of) an existing fact 65 | foreach ($record->facts([], false, null, true) as $fact) { 66 | if ($fact->id() === $fact_id && $fact->canEdit()) { 67 | $record->updateFact($fact_id, $gedcom, !$keep_chan); 68 | break; 69 | } 70 | } 71 | } 72 | 73 | //new facts 74 | foreach ($new_ids as $new_id) { 75 | $levels = Validator::parsedBody($request)->array('new-'.$new_id.'-levels'); 76 | $tags = Validator::parsedBody($request)->array('new-'.$new_id.'-tags'); 77 | $values = Validator::parsedBody($request)->array('new-'.$new_id.'-values'); 78 | $gedcom = $this->gedcom_edit_service->editLinesToGedcom(Individual::RECORD_TYPE, $levels, $tags, $values); 79 | 80 | $record->updateFact('', $gedcom, !$keep_chan); 81 | } 82 | 83 | $url = Validator::parsedBody($request)->isLocalUrl()->string('url', $record->url()); 84 | 85 | return redirect($url); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PlaceholderModule.php: -------------------------------------------------------------------------------- 1 | minRequiredWebtreesVersion(); 52 | 53 | //min version check 54 | $version_ok = version_compare(Webtrees::VERSION, $min_version) >= 0; 55 | if (!$version_ok) { 56 | return CommonI18N::noopModuleMin($min_version); 57 | } 58 | 59 | $max_version = $this->minUnsupportedWebtreesVersion(); 60 | 61 | //max version check (allow current dev version though) 62 | $version_ok = (Webtrees::VERSION === $max_version.'-dev') || (version_compare($max_version, Webtrees::VERSION) > 0); 63 | if (!$version_ok) { 64 | return CommonI18N::noopModuleMax($max_version); 65 | } 66 | 67 | return ''; 68 | } 69 | 70 | public function ifIncompatible(): ?PlaceholderModule { 71 | $min_version = $this->minRequiredWebtreesVersion(); 72 | 73 | //min version check 74 | $version_ok = version_compare(Webtrees::VERSION, $min_version) >= 0; 75 | if (!$version_ok) { 76 | return $this; 77 | } 78 | 79 | $max_version = $this->minUnsupportedWebtreesVersion(); 80 | 81 | //max version check (allow current dev version though) 82 | $version_ok = (Webtrees::VERSION === $max_version.'-dev') || (version_compare($max_version, Webtrees::VERSION) > 0); 83 | if (!$version_ok) { 84 | return $this; 85 | } 86 | 87 | return null; 88 | } 89 | 90 | public function boot(): void { 91 | //flash, but only once per day 92 | $title = $this->title(); 93 | 94 | $cache = Registry::cache()->file(); 95 | $key = $title . '-placeholder-flash'; 96 | $cache->remember($key, function () use ($title) { 97 | FlashMessages::addMessage(CommonI18N::noopModuleMessage($title)); 98 | }, 24*3600); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /patchedWebtrees/CommonMark/CustomCensusTableParser.php: -------------------------------------------------------------------------------- 1 | getContainer(); 54 | 55 | if (!$container instanceof Paragraph) { 56 | return false; 57 | } 58 | 59 | $lines = $container->getStrings(); 60 | $first = array_shift($lines); 61 | 62 | //[PATCHED] 63 | if (($first !== self::CA_PREFIX) && ($first !== self::CA_PREFIX2)) { 64 | return false; 65 | } 66 | 67 | //[PATCHED] 68 | if (($cursor->getLine() !== self::CA_SUFFIX) && ($cursor->getLine() !== self::CA_SUFFIX2)) { 69 | return false; 70 | } 71 | 72 | // We don't need to parse/markup any of the table's contents. 73 | $table = new Table(static function (): bool { 74 | return false; 75 | }); 76 | 77 | // First line is the table header. 78 | $line = array_shift($lines); 79 | $row = $this->parseRow($line, TableCell::TYPE_HEAD); 80 | $table->getHead()->appendChild($row); 81 | 82 | // Subsequent lines are the table body. 83 | while ($lines !== []) { 84 | $line = array_shift($lines); 85 | $row = $this->parseRow($line, TableCell::TYPE_BODY); 86 | $table->getBody()->appendChild($row); 87 | } 88 | 89 | $context->replaceContainerBlock($table); 90 | 91 | return true; 92 | } 93 | 94 | /** 95 | * @param string $line 96 | * @param string $type 97 | * 98 | * @return TableRow 99 | */ 100 | private function parseRow(string $line, string $type): TableRow 101 | { 102 | $cells = explode('|', $line); 103 | $row = new TableRow(); 104 | 105 | foreach ($cells as $cell) { 106 | if (str_starts_with($cell, self::TH_PREFIX)) { 107 | $cell = substr($cell, strlen(self::TH_PREFIX)); 108 | $type = TableCell::TYPE_HEAD; 109 | } 110 | 111 | $row->appendChild(new TableCell($cell, $type, null)); 112 | } 113 | 114 | return $row; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /patchedWebtrees/GedcomRecordPageTempReplacement.php: -------------------------------------------------------------------------------- 1 | clipboard_service = $clipboard_service; 36 | $this->linked_record_service = $linked_record_service; 37 | } 38 | 39 | /** 40 | * Show a gedcom record's page. 41 | * 42 | * @param ServerRequestInterface $request 43 | * 44 | * @return ResponseInterface 45 | */ 46 | public function handle(ServerRequestInterface $request): ResponseInterface 47 | { 48 | $tree = Validator::attributes($request)->tree(); 49 | $xref = Validator::attributes($request)->isXref()->string('xref'); 50 | $record = Registry::gedcomRecordFactory()->make($xref, $tree); 51 | $record = Auth::checkRecordAccess($record); 52 | 53 | // Standard genealogy records have their own pages. 54 | $genericRecord = Registry::gedcomRecordFactory()->new($xref, '', null, $tree); 55 | if ($record->url() !== $genericRecord->url()) { 56 | return redirect($record->url()); 57 | } 58 | 59 | $linked_families = $this->linked_record_service->linkedFamilies($record); 60 | $linked_individuals = $this->linked_record_service->linkedIndividuals($record); 61 | $linked_locations = $this->linked_record_service->linkedLocations($record); 62 | $linked_media = $this->linked_record_service->linkedMedia($record); 63 | $linked_notes = $this->linked_record_service->linkedNotes($record); 64 | $linked_repositories = $this->linked_record_service->linkedRepositories($record); 65 | $linked_sources = $this->linked_record_service->linkedSources($record); 66 | $linked_submitters = $this->linked_record_service->linkedSubmitters($record); 67 | 68 | return $this->viewResponse('record-page', [ 69 | 'clipboard_facts' => $this->clipboard_service->pastableFacts($record), 70 | 'linked_families' => $linked_families->isEmpty() ? null : $linked_families, 71 | 'linked_individuals' => $linked_individuals->isEmpty() ? null : $linked_individuals, 72 | 'linked_locations' => $linked_locations->isEmpty() ? null : $linked_locations, 73 | 'linked_media_objects' => $linked_media->isEmpty() ? null : $linked_media, 74 | 'linked_notes' => $linked_notes->isEmpty() ? null : $linked_notes, 75 | 'linked_repositories' => $linked_repositories->isEmpty() ? null : $linked_repositories, 76 | 'linked_sources' => $linked_sources->isEmpty() ? null : $linked_sources, 77 | 'linked_submitters' => $linked_submitters->isEmpty() ? null : $linked_submitters, 78 | 'record' => $record, 79 | 'title' => $record->fullName(), 80 | 'tree' => $tree, 81 | ]); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ⚶ Vesta Classic Look & Feel (Webtrees Custom Module) 3 | 4 | This [webtrees](https://www.webtrees.net/) custom module adjusts all themes and other features, providing a look & feel closer to the webtrees 1.x version. 5 | The project’s website is [cissee.de](https://cissee.de). 6 | 7 | ## Contents 8 | 9 | * [Features](#features) 10 | * [Download](#download) 11 | * [Installation](#installation) 12 | * [License](#license) 13 | 14 | ### Features 15 | 16 | #### Layout 17 | 18 | * The overall width is adjusted for larger resolutions, as suggested [here](https://www.webtrees.net/index.php/en/forum/3-help-for-2-0-alpha/32882-solved-support-for-bigger-monitors#70135) 19 | * The individual page is adjusted further: 20 | * Smaller sidebar 21 | * Less padding between elements 22 | * The key-value pairs of the name parts are inline 23 | * Gender information is moved to the header (as an icon) 24 | * Media edit controls is moved to the edit menu (a better place for these edit controls may be the Media tab itself) 25 | 26 | default 'webtrees' theme | adjusted 'webtrees' theme 27 | :-------------------------:|:-------------------------: 28 | ![Screenshot](individual.png) | ![Screenshot](individual_compact.png) 29 | 30 | * All edit dialogs are also displayed in a more compact layout. 31 | * Note that this module itself is not a theme: The webtrees user will not be able to switch between the compact and the regular layout! All layout adjustments are globally configurable though. 32 | * Further suggestions are very welcome! 33 | 34 | #### Functionality 35 | 36 | * The module optionally displays nicknames as in webtrees 1.x (before the surname). See [here](https://github.com/fisharebest/webtrees/issues/1401) for the related discussion. 37 | * The module allows to use xrefs with specific prefixes, as in webtrees 1.x. See [e.g. here](https://www.webtrees.net/index.php/en/forum/help-for-2-0/33978-identities-in-gedcom-file) for the related discussion. 38 | * The module provides fine-grained configuration options for all GEDCOM tags, as described [here](https://github.com/vesta-webtrees-2-custom-modules/vesta_common/blob/master/docs/GEDCOMTags.md). 39 | 40 | ### Download 41 | 42 | * Current version: 2.2.4.1.0 43 | * Based on and tested with webtrees 2.2.4. Requires webtrees 2.2.1 or later. 44 | * Requires the ⚶ Vesta Common module ('vesta_common'). 45 | * Download the zip file, which includes all Vesta modules, [here](https://cissee.de/vesta.latest.zip). 46 | * Support, suggestions, feature requests: 47 | * Issues also via 48 | * Translations may be contributed via weblate: 49 | 50 | ### Installation 51 | 52 | * Unzip the files and copy the contents of the modules_v4 folder to the respective folder of your webtrees installation. All related modules are included in the zip file. It's safe to overwrite the respective directories if they already exist (they are bundled with other custom modules as well), as long as other custom models using these dependencies are also upgraded to their respective latest versions. 53 | * Enable the main module via Control Panel -> Modules -> All modules -> ⚶ Vesta Classic Look & Feel. After that, you may configure some options. 54 | 55 | ### License 56 | 57 | * **vesta_classic_look_and_feel: a webtrees custom module** 58 | * Copyright (C) 2020 – 2025 Richard Cissée 59 | * Derived from **webtrees** - Copyright 2022 webtrees development team. 60 | * Dutch translations provided by TheDutchJewel. 61 | * Czech translations provided by Josef Prause. 62 | 63 | This program is free software: you can redistribute it and/or modify 64 | it under the terms of the GNU General Public License as published by 65 | the Free Software Foundation, either version 3 of the License, or 66 | (at your option) any later version. 67 | 68 | This program is distributed in the hope that it will be useful, 69 | but WITHOUT ANY WARRANTY; without even the implied warranty of 70 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 71 | GNU General Public License for more details. 72 | 73 | You should have received a copy of the GNU General Public License 74 | along with this program. If not, see . 75 | -------------------------------------------------------------------------------- /resources/views/edit/edit-gedcom-fields-ext2.phtml: -------------------------------------------------------------------------------- 1 | $hierarchy 11 | * @var string $prefix 12 | * @var Tree $tree 13 | */ 14 | 15 | preg_match_all('/^(\d+) (\w+) ?(.*)/m', $gedcom, $matches); 16 | [, $levels, $tags, $values] = $matches; 17 | $levels = array_map(static fn (string $x): int => (int) $x, $levels); 18 | $keys = array_keys($levels); 19 | $elements = []; 20 | $ids = []; 21 | $indent = []; 22 | $collapse = []; 23 | $collapsed = []; 24 | 25 | foreach ($keys as $num => $key) { 26 | $hierarchy[$levels[$key]] = $tags[$key]; 27 | $full_tag = implode(':', array_slice($hierarchy, 0, 1 + $levels[$key])); 28 | $full_tag2 = implode('-', array_slice($hierarchy, 0, 1 + $levels[$key])); 29 | 30 | $elements[$key] = Registry::elementFactory()->make($full_tag); 31 | $ids[$key] = Registry::idFactory()->id() . '-' . $full_tag2; 32 | 33 | // Does this element have any children? 34 | $has_subtags = ($levels[$key + 1] ?? 0) > $levels[$key]; 35 | 36 | // Do these children have values? 37 | $has_subtags_with_values = false; 38 | for ($n = $key + 1; $n < count($keys) && $levels[$n] > $levels[$key]; ++$n) { 39 | if ($values[$n] !== '') { 40 | $has_subtags_with_values = true; 41 | break; 42 | } 43 | } 44 | 45 | $indent[$key] = $elements[$key]->collapseChildren() && $has_subtags; 46 | 47 | $collapse[$key] = $num > 0 && $indent[$key] && !$has_subtags_with_values; 48 | $collapsed[$key] = $collapse[$key]; 49 | 50 | //[RC] adjusted: maybe override collapse 51 | $ges = \Vesta\VestaUtils::get(GedcomEditServiceExt2::class); 52 | if ($ges->alwaysExpandSubtags($tree, $full_tag)) { 53 | $collapsed[$key] = false; 54 | } 55 | } 56 | 57 | ?> 58 | 59 | 60 | 61 |
62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 | 84 | 85 |
86 | 87 | 88 | edit($ids[$key], $prefix . 'values[]', strtr($values[$key], ["\r" => "\n"]), $tree) ?> 89 |
90 |
91 | 92 | 93 | = ($levels[$key + 1] ?? $levels[0]); $n--) : ?> 94 | 95 | 96 |
97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /resources/views/individual-page-images.phtml: -------------------------------------------------------------------------------- 1 | $individual_media 12 | * @var Collection $name_records 13 | * //[RC] adjusted 14 | * @var IndividualExt $record 15 | * @var Collection $sex_records 16 | * @var Collection $shares 17 | * @var Collection $sidebars 18 | * @var Collection $tabs 19 | * @var Tree $tree 20 | * @var string $user_link 21 | */ 22 | 23 | $compactIndividualPage = $record->settings()->compactIndividualPage(); 24 | $cropThumbnails = $record->settings()->cropThumbnails(); 25 | ?> 26 | 27 | isNotEmpty() || $tree->getPreference('USE_SILHOUETTE') === '1') : ?> 28 | 29 | 32 | ?> 33 |
34 | 35 | 92 | 93 | -------------------------------------------------------------------------------- /resources/views/family-page-menu.phtml: -------------------------------------------------------------------------------- 1 | $clipboard_facts 21 | * @var Family $record 22 | */ 23 | 24 | ?> 25 | 26 | 99 | -------------------------------------------------------------------------------- /resources/views/edit/edit-gedcom-fields-ext.phtml: -------------------------------------------------------------------------------- 1 | $hierarchy 12 | * @var string $prefix 13 | * @var Tree $tree 14 | */ 15 | 16 | preg_match_all('/^(\d+) (\w+) ?(.*)/m', $gedcom, $matches); 17 | [, $levels, $tags, $values] = $matches; 18 | $levels = array_map(static fn (string $x): int => (int) $x, $levels); 19 | $keys = array_keys($levels); 20 | $elements = []; 21 | $ids = []; 22 | $indent = []; 23 | $collapse = []; 24 | $collapsed = []; 25 | 26 | $full_tags = []; 27 | 28 | foreach ($keys as $num => $key) { 29 | $hierarchy[$levels[$key]] = $tags[$key]; 30 | $full_tag = implode(':', array_slice($hierarchy, 0, 1 + $levels[$key])); 31 | $full_tag2 = implode('-', array_slice($hierarchy, 0, 1 + $levels[$key])); 32 | 33 | $full_tags[$key] = $full_tag; 34 | 35 | $elements[$key] = Registry::elementFactory()->make($full_tag); 36 | $ids[$key] = Registry::idFactory()->id() . '-' . $full_tag2; 37 | 38 | // Does this element have any children? 39 | $has_subtags = ($levels[$key + 1] ?? 0) > $levels[$key]; 40 | 41 | // Do these children have values? 42 | $has_subtags_with_values = false; 43 | for ($n = $key + 1; $n < count($keys) && $levels[$n] > $levels[$key]; ++$n) { 44 | if ($values[$n] !== '') { 45 | $has_subtags_with_values = true; 46 | break; 47 | } 48 | } 49 | 50 | $indent[$key] = $elements[$key]->collapseChildren() && $has_subtags; 51 | 52 | $collapse[$key] = $num > 0 && $indent[$key] && !$has_subtags_with_values; 53 | $collapsed[$key] = $collapse[$key]; 54 | 55 | //[RC] adjusted: maybe override collapse 56 | $ges = \Vesta\VestaUtils::get(GedcomEditServiceExt2::class); 57 | if ($ges->alwaysExpandSubtags($tree, $full_tag)) { 58 | $collapsed[$key] = false; 59 | } 60 | } 61 | 62 | ?> 63 | 64 | 65 | 66 | 67 |
68 | 69 |
70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 101 | 102 |
103 | 104 | 105 | edit($ids[$key], $prefix . 'values[]', strtr($values[$key], ["\r" => "\n"]), $tree) ?> 106 |
107 |
108 | 109 | 110 | = ($levels[$key + 1] ?? $levels[0]); $n--) : ?> 111 | 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 125 | -------------------------------------------------------------------------------- /resources/views/admin/badges/config.phtml: -------------------------------------------------------------------------------- 1 | $nameBadges 14 | * @var int $max_block_order 15 | * @var int $min_block_order 16 | * @var string $module 17 | * @var string $title 18 | * @var Tree $tree 19 | * @var array $tree_names 20 | */ 21 | 22 | $accessLevelNames = Auth::accessLevelNames(); 23 | ?> 24 | 25 | [route(ControlPanel::class) => MoreI18N::xlate('Control panel'), route(ModulesAllPage::class) => MoreI18N::xlate('Modules'), $title]]) ?> 26 | 27 |

28 | 29 |

30 | 31 | 32 | 33 | 34 |

35 | 36 |
37 |
38 |
39 | 'tree', 'selected' => $tree->name(), 'options' => $tree_names, 'aria_label' => MoreI18N::xlate('Family tree')]) ?> 40 | 41 |
42 |
43 | 44 | 45 |
46 | 47 |

48 | 49 | 50 | 51 | 52 |

53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | 86 | 89 | 96 | 103 | 108 | 113 | 114 | 115 | 116 |
56 | 57 |
75 | block_order) ?> 76 | 78 | gedcom_id === null) : ?> 79 | 80 | 81 | title()) ?> 82 | 83 |
84 | access]) ?> 85 |
87 | header) ?> 88 | 90 | block_order !== $min_block_order) : ?> 91 | 92 | 93 | 94 | 95 | 97 | block_order !== $max_block_order) : ?> 98 | 99 | 100 | 101 | 102 | 104 | 105 | 106 | 107 | 109 | 110 | 111 | 112 |
117 | -------------------------------------------------------------------------------- /resources/views/admin/badges/edit.phtml: -------------------------------------------------------------------------------- 1 | $gedcom_ids 16 | * @var string $header 17 | * @var string $note 18 | * @var string $regex 19 | * @var string $snippet 20 | * @var int $access 21 | * @var bool $prefix 22 | * @var string $title 23 | * @var string $module 24 | * 25 | */ 26 | 27 | ?> 28 | 29 | [route(ControlPanel::class) => MoreI18N::xlate('Control panel'), route(ModulesAllPage::class) => MoreI18N::xlate('Modules'), route('module', ['module' => $module, 'action' => 'Admin2']) => I18N::translate('Name badges'), $title]]) ?> 30 | 31 |

32 | 33 |
34 | 35 | 36 |
37 | 40 | 41 |
42 | 44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | 55 | 56 |
57 | 58 | 59 |
60 | 61 |
62 |
63 |
64 | 65 |
66 | 69 | 70 |
71 | 73 | 74 |
75 | 76 | 77 | 78 | 79 |
80 |
81 |
82 | 83 |
84 | 87 | 88 |
89 | 90 | 91 |
92 | 95 |
96 |
97 |
98 | 99 |
100 | 103 | 104 |
105 | 106 |
107 |
108 | 109 |
110 | 113 | 114 |
115 | > 116 |
117 |
118 | 119 |
120 | 123 | 124 |
125 | 'access', 'selected' => $access, 'options' => Auth::accessLevelNames()]) ?> 126 |
127 |
128 | 129 | 130 | 131 |
132 | 135 | 136 |
137 | 'gedcom_id', 'selected' => $gedcom_id, 'options' => $gedcom_ids]) ?> 138 |
139 | 140 |
141 |
142 |
143 | 144 |
145 |
146 | 150 |
151 |
152 | 153 | 154 |
155 | -------------------------------------------------------------------------------- /resources/views/individual-name.phtml: -------------------------------------------------------------------------------- 1 | record(); 16 | $tree = $individual->tree(); 17 | 18 | // Create a fake record, so we can extract the formatted NAME value from it. 19 | $fake_individual = Registry::individualFactory()->new( 20 | 'xref', 21 | "0 @xref@ INDI\n1 DEAT Y\n" . $fact->gedcom(), 22 | null, 23 | $tree 24 | ); 25 | $fake_individual->setPrimaryName(0); // Make sure we use the name from "1 NAME" 26 | 27 | $container_class = ''; 28 | 29 | if ($fact->isPendingDeletion()) { 30 | $container_class = 'wt-old'; 31 | } elseif ($fact->isPendingAddition()) { 32 | $container_class = 'wt-new'; 33 | } 34 | 35 | $module = \Vesta\VestaUtils::get(ClassicLAFModule::class); 36 | $autoExpandValue = intval($module->getPreference('EXPAND_NAME', '0')); 37 | 38 | $autoExpand = false; 39 | if ($autoExpandValue === 1) { 40 | //cf fact-notes.phtml/fact-sources.phtml 41 | $hasNotes = (preg_match_all('/\n(2 NOTE\b.*(?:\n[^2].*)*)/', $fact->gedcom(), $matches, PREG_SET_ORDER) > 0); 42 | $hasSources = (preg_match_all('/\n(2 SOUR\b.*(?:\n[^2].*)*)/', $fact->gedcom(), $matches, PREG_SET_ORDER) > 0); 43 | 44 | if ($hasNotes || $hasSources) { 45 | $autoExpand = true; 46 | } 47 | } else if ($autoExpandValue === 2) { 48 | $autoExpand = true; 49 | } 50 | 51 | if ($autoExpand) { 52 | $collapsed = ""; 53 | $ariaExpanded = "true"; 54 | $show = " show"; 55 | } else { 56 | $collapsed = " collapsed"; 57 | $ariaExpanded = "false"; 58 | $show = ""; 59 | } 60 | 61 | ?> 62 |
63 |
64 | 97 |
98 | 99 | 103 |
104 |
105 |
106 |
107 |
value()) ?>
108 | 109 | gedcom(), $matches, PREG_SET_ORDER) ?> 110 | $match) : ?> 111 | 112 | make($fact->tag() . ':' . $tag) ?> 113 | 114 |
115 | label() ?> 116 |
117 |
118 | value($value, $fact->record()->tree()) ?> 119 |
120 | 121 | 122 |
123 | 124 | $fact]) ?> 125 | $fact]) ?> 126 | 127 | 128 |
129 |
130 |
131 | -------------------------------------------------------------------------------- /resources/views/layouts/defaultJustLight.phtml: -------------------------------------------------------------------------------- 1 | palette(); 41 | switch ($palette) { 42 | case 'justlight': 43 | $color_scheme = 'light'; 44 | break; 45 | case 'justblack': 46 | $color_scheme = 'dark'; 47 | break; 48 | default: 49 | $color_scheme = isset($_COOKIE["JL_COLOR_SCHEME"]) ? $_COOKIE["JL_COLOR_SCHEME"] : false; 50 | if ($color_scheme === false) $color_scheme = 'light'; // fallback 51 | } 52 | 53 | ?> 54 | 55 | 56 | 57 | 58 | name() . '::layouts/head/meta', [ 59 | 'meta_robots' => e($meta_robots ?? 'noindex'), 60 | 'meta_description' => $meta_description ?? '' 61 | ]); ?> 62 | name() . '::layouts/head/title', ['tree' => $tree, 'title' => $title]); ?> 63 | name() . '::layouts/head/favicons'); ?> 64 | 65 | 66 | 67 | name() . '::layouts/head/stylesheets'); ?> 68 | 69 | 70 | 71 | 72 | 76 | 77 |
78 |
79 | 96 | 97 | name() . '::layouts/body/primary-navigation', ['tree' => $tree]); ?> 98 | 99 | name() . '::layouts/body/mobile-navigation', ['tree' => $tree]); ?> 100 |
101 |
102 | 103 | 104 |
105 | name() . '::layouts/body/flash-messages'); ?> 106 | 107 | 110 | 111 |
112 | 113 |
114 | 115 | 116 |
117 |
118 | 119 | 122 | 123 | name() . '::theme/footer'); ?> 124 | 125 | 126 | name() . '::layouts/body/scripts'); ?> 127 | 128 | 129 | -------------------------------------------------------------------------------- /resources/views/individual-page-menu.phtml: -------------------------------------------------------------------------------- 1 | $clipboard_facts 21 | * @var Individual $record 22 | * @var Collection $shares 23 | */ 24 | 25 | ?> 26 | 27 | 139 | -------------------------------------------------------------------------------- /resources/views/chart-box.phtml: -------------------------------------------------------------------------------- 1 |
'; 25 | 26 | return; 27 | } 28 | 29 | $module_service = \Vesta\VestaUtils::get(ModuleService::class); 30 | 31 | $menus = $module_service->findByComponent(ModuleChartInterface::class, $individual->tree(), Auth::user())->map(static function (ModuleChartInterface $module) use ($individual): ?Menu { 32 | return $module->chartBoxMenu($individual); 33 | })->filter(); 34 | 35 | foreach ($individual->spouseFamilies() as $family) { 36 | $menus->push(new Menu('' . MoreI18N::xlate('Family with spouse') . '', $family->url())); 37 | $spouse = $family->spouse($individual); 38 | if ($spouse && $spouse->canShow()) { 39 | $menus->push(new Menu($spouse->fullName(), $spouse->url())); 40 | } 41 | foreach ($family->children() as $child) { 42 | if ($child->canShow()) { 43 | $menus->push(new Menu($child->fullName(), $child->url())); 44 | } 45 | } 46 | } 47 | 48 | // Do not show these facts in the expanded chart boxes. 49 | $exclude = [ 50 | 'FAM:CHAN', 51 | 'FAM:CHIL', 52 | 'FAM:HUSB', 53 | 'FAM:NOTE', 54 | 'FAM:OBJE', 55 | 'FAM:RESN', 56 | 'FAM:SOUR', 57 | 'FAM:WIFE', 58 | 'INDI:ADDR', 59 | 'INDI:ALIA', 60 | 'INDI:ASSO', 61 | 'INDI:CHAN', 62 | 'INDI:EMAIL', 63 | 'INDI:FAMC', 64 | 'INDI:FAMS', 65 | 'INDI:NAME', 66 | 'INDI:NOTE', 67 | 'INDI:OBJE', 68 | 'INDI:PHON', 69 | 'INDI:RESI', 70 | 'INDI:RESN', 71 | 'INDI:SEX', 72 | 'INDI:SOUR', 73 | 'INDI:SSN', 74 | 'INDI:SUBM', 75 | 'INDI:TITL', 76 | 'INDI:URL', 77 | 'INDI:WWW', 78 | 'INDI:_EMAIL', 79 | 'INDI:_TODO', 80 | 'INDI:_UID', 81 | 'INDI:_WT_OBJE_SORT' 82 | ]; 83 | 84 | /** @var Collection|Fact[] $all_facts */ 85 | $all_facts = $individual->facts(); 86 | foreach ($individual->spouseFamilies() as $family) { 87 | foreach ($family->facts() as $fact) { 88 | $all_facts->push($fact); 89 | } 90 | } 91 | 92 | $all_facts = $all_facts->filter(static function (Fact $fact) use ($exclude): bool { 93 | return !in_array($fact->tag(), $exclude, true); 94 | }); 95 | 96 | $all_facts = Fact::sortFacts($all_facts); 97 | 98 | $id = Uuid::uuid4()->toString(); 99 | ?> 100 | 101 |
102 | canShow() && $individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) : ?> 103 |
104 | 107 | displayImage(40, 50, 'contain', ['class' => 'wt-chart-box-thumbnail']) ?> 108 |
109 | 110 | 111 | canShow()) : ?> 112 |
113 | 126 | 127 | 141 |
142 | 143 | 144 |
145 | canShow()) : ?> 146 | fullName() ?> 147 | 148 | fullName() ?> 149 | 150 |
151 | 152 |
153 | alternateName() ?> 154 |
155 | 156 |
157 | lifespan() ?> 158 |
159 | 160 |
161 |
162 | tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 164 | // Show BIRT or equivalent event 165 | 166 | foreach (Gedcom::BIRTH_EVENTS as $birttag) { 167 | if (!in_array($birttag, $opt_tags, true)) { 168 | $event = $individual->facts([$birttag])->first(); 169 | if ($event instanceof Fact) { 170 | echo $event->summary(); 171 | break; 172 | } 173 | } 174 | } 175 | // Show optional events (before death) 176 | foreach ($opt_tags as $key => $tag) { 177 | if (!in_array($tag, Gedcom::DEATH_EVENTS, true)) { 178 | $event = $individual->facts([$tag])->first(); 179 | if ($event instanceof Fact) { 180 | echo $event->summary(); 181 | unset($opt_tags[$key]); 182 | } 183 | } 184 | } 185 | // Show DEAT or equivalent event 186 | foreach (Gedcom::DEATH_EVENTS as $deattag) { 187 | $event = $individual->facts([$deattag])->first(); 188 | if ($event instanceof Fact) { 189 | echo $event->summary(); 190 | if (in_array($deattag, $opt_tags, true)) { 191 | unset($opt_tags[array_search($deattag, $opt_tags, true)]); 192 | } 193 | break; 194 | } 195 | } 196 | // Show remaining optional events (after death) 197 | foreach ($opt_tags as $tag) { 198 | $event = $individual->facts([$tag])->first(); 199 | if ($event instanceof Fact) { 200 | echo $event->summary(); 201 | } 202 | } 203 | ?> 204 |
205 |
206 |
207 | -------------------------------------------------------------------------------- /IvoPetkov/HTML5DOMTokenList.php: -------------------------------------------------------------------------------- 1 | element = $element; 53 | $this->attributeName = $attributeName; 54 | $this->previousValue = null; 55 | $this->tokenize(); 56 | } 57 | 58 | /** 59 | * Adds the given tokens to the list. 60 | * 61 | * @param string[] $tokens The tokens you want to add to the list. 62 | * @return void 63 | */ 64 | public function add(string ...$tokens) 65 | { 66 | if (count($tokens) === 0) { 67 | return; 68 | } 69 | foreach ($tokens as $t) { 70 | if (in_array($t, $this->tokens)) { 71 | continue; 72 | } 73 | $this->tokens[] = $t; 74 | } 75 | $this->setAttributeValue(); 76 | } 77 | 78 | /** 79 | * Removes the specified tokens from the list. If the string does not exist in the list, no error is thrown. 80 | * 81 | * @param string[] $tokens The token you want to remove from the list. 82 | * @return void 83 | */ 84 | public function remove(string ...$tokens) 85 | { 86 | if (count($tokens) === 0) { 87 | return; 88 | } 89 | if (count($this->tokens) === 0) { 90 | return; 91 | } 92 | foreach ($tokens as $t) { 93 | $i = array_search($t, $this->tokens); 94 | if ($i === false) { 95 | continue; 96 | } 97 | array_splice($this->tokens, $i, 1); 98 | } 99 | $this->setAttributeValue(); 100 | } 101 | 102 | /** 103 | * Returns an item in the list by its index (returns null if the number is greater than or equal to the length of the list). 104 | * 105 | * @param int $index The zero-based index of the item you want to return. 106 | * @return null|string 107 | */ 108 | public function item(int $index) 109 | { 110 | $this->tokenize(); 111 | if ($index >= count($this->tokens)) { 112 | return null; 113 | } 114 | return $this->tokens[$index]; 115 | } 116 | 117 | /** 118 | * Removes a given token from the list and returns false. If token doesn't exist it's added and the function returns true. 119 | * 120 | * @param string $token The token you want to toggle. 121 | * @param bool $force A Boolean that, if included, turns the toggle into a one way-only operation. If set to false, the token will only be removed but not added again. If set to true, the token will only be added but not removed again. 122 | * @return bool false if the token is not in the list after the call, or true if the token is in the list after the call. 123 | */ 124 | public function toggle(string $token, bool $force = null): bool 125 | { 126 | $this->tokenize(); 127 | $isThereAfter = false; 128 | $i = array_search($token, $this->tokens); 129 | if (is_null($force)) { 130 | if ($i === false) { 131 | $this->tokens[] = $token; 132 | $isThereAfter = true; 133 | } else { 134 | array_splice($this->tokens, $i, 1); 135 | } 136 | } else { 137 | if ($force) { 138 | if ($i === false) { 139 | $this->tokens[] = $token; 140 | } 141 | $isThereAfter = true; 142 | } else { 143 | if ($i !== false) { 144 | array_splice($this->tokens, $i, 1); 145 | } 146 | } 147 | } 148 | $this->setAttributeValue(); 149 | return $isThereAfter; 150 | } 151 | 152 | /** 153 | * Returns true if the list contains the given token, otherwise false. 154 | * 155 | * @param string $token The token you want to check for the existence of in the list. 156 | * @return bool true if the list contains the given token, otherwise false. 157 | */ 158 | public function contains(string $token): bool 159 | { 160 | $this->tokenize(); 161 | return in_array($token, $this->tokens); 162 | } 163 | 164 | /** 165 | * Replaces an existing token with a new token. 166 | * 167 | * @param string $old The token you want to replace. 168 | * @param string $new The token you want to replace $old with. 169 | * @return void 170 | */ 171 | public function replace(string $old, string $new) 172 | { 173 | if ($old === $new) { 174 | return; 175 | } 176 | $this->tokenize(); 177 | $i = array_search($old, $this->tokens); 178 | if ($i !== false) { 179 | $j = array_search($new, $this->tokens); 180 | if ($j === false) { 181 | $this->tokens[$i] = $new; 182 | } else { 183 | array_splice($this->tokens, $i, 1); 184 | } 185 | $this->setAttributeValue(); 186 | } 187 | } 188 | 189 | /** 190 | * 191 | * @return string 192 | */ 193 | public function __toString(): string 194 | { 195 | $this->tokenize(); 196 | return implode(' ', $this->tokens); 197 | } 198 | 199 | /** 200 | * Returns an iterator allowing you to go through all tokens contained in the list. 201 | * 202 | * @return ArrayIterator 203 | */ 204 | public function entries(): ArrayIterator 205 | { 206 | $this->tokenize(); 207 | return new ArrayIterator($this->tokens); 208 | } 209 | 210 | /** 211 | * Returns the value for the property specified 212 | * 213 | * @param string $name The name of the property 214 | * @return string The value of the property specified 215 | * @throws \Exception 216 | */ 217 | public function __get(string $name) 218 | { 219 | if ($name === 'length') { 220 | $this->tokenize(); 221 | return count($this->tokens); 222 | } else if ($name === 'value') { 223 | return $this->__toString(); 224 | } 225 | throw new \Exception('Undefined property: HTML5DOMTokenList::$' . $name); 226 | } 227 | 228 | /** 229 | * 230 | * @return void 231 | */ 232 | private function tokenize() 233 | { 234 | $current = $this->element->getAttribute($this->attributeName); 235 | if ($this->previousValue === $current) { 236 | return; 237 | } 238 | $this->previousValue = $current; 239 | $tokens = explode(' ', $current); 240 | $finals = []; 241 | foreach ($tokens as $token) { 242 | if ($token === '') 243 | continue; 244 | if (in_array($token, $finals)) 245 | continue; 246 | $finals[] = $token; 247 | } 248 | $this->tokens = $finals; 249 | } 250 | 251 | /** 252 | * 253 | * @return void 254 | */ 255 | private function setAttributeValue() 256 | { 257 | $value = implode(' ', $this->tokens); 258 | if ($this->previousValue === $value) { 259 | return; 260 | } 261 | $this->previousValue = $value; 262 | $this->element->setAttribute($this->attributeName, $value); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /IvoPetkov/HTML5DOMElement.php: -------------------------------------------------------------------------------- 1 | firstChild === null) { 53 | return ''; 54 | } 55 | $html = $this->ownerDocument->saveHTML($this); 56 | $nodeName = $this->nodeName; 57 | return preg_replace('@^<' . $nodeName . '[^>]*>|$@', '', $html); 58 | } elseif ($name === 'outerHTML') { 59 | if ($this->firstChild === null) { 60 | $nodeName = $this->nodeName; 61 | $attributes = $this->getAttributes(); 62 | $result = '<' . $nodeName . ''; 63 | foreach ($attributes as $name => $value) { 64 | $result .= ' ' . $name . '="' . htmlentities($value) . '"'; 65 | } 66 | if (array_search($nodeName, ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']) === false) { 67 | $result .= '>'; 68 | } else { 69 | $result .= '/>'; 70 | } 71 | return $result; 72 | } 73 | return $this->ownerDocument->saveHTML($this); 74 | } elseif ($name === 'classList') { 75 | if ($this->classList === null) { 76 | $this->classList = new HTML5DOMTokenList($this, 'class'); 77 | } 78 | return $this->classList; 79 | } 80 | throw new \Exception('Undefined property: HTML5DOMElement::$' . $name); 81 | } 82 | 83 | /** 84 | * Sets the value for the property specified. 85 | * 86 | * @param string $name 87 | * @param string $value 88 | * @throws \Exception 89 | */ 90 | public function __set(string $name, $value) 91 | { 92 | if ($name === 'innerHTML') { 93 | while ($this->hasChildNodes()) { 94 | $this->removeChild($this->firstChild); 95 | } 96 | if (!isset(self::$newObjectsCache['html5domdocument'])) { 97 | self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument(); 98 | } 99 | $tmpDoc = clone (self::$newObjectsCache['html5domdocument']); 100 | $tmpDoc->loadHTML('' . $value . ''); 101 | foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) { 102 | $node = $this->ownerDocument->importNode($node, true); 103 | $this->appendChild($node); 104 | } 105 | return; 106 | } elseif ($name === 'outerHTML') { 107 | if (!isset(self::$newObjectsCache['html5domdocument'])) { 108 | self::$newObjectsCache['html5domdocument'] = new \IvoPetkov\HTML5DOMDocument(); 109 | } 110 | $tmpDoc = clone (self::$newObjectsCache['html5domdocument']); 111 | $tmpDoc->loadHTML('' . $value . ''); 112 | foreach ($tmpDoc->getElementsByTagName('body')->item(0)->childNodes as $node) { 113 | $node = $this->ownerDocument->importNode($node, true); 114 | $this->parentNode->insertBefore($node, $this); 115 | } 116 | $this->parentNode->removeChild($this); 117 | return; 118 | } elseif ($name === 'classList') { 119 | $this->setAttribute('class', $value); 120 | return; 121 | } 122 | throw new \Exception('Undefined property: HTML5DOMElement::$' . $name); 123 | } 124 | 125 | /** 126 | * Updates the result value before returning it. 127 | * 128 | * @param string $value 129 | * @return string The updated value 130 | */ 131 | private function updateResult(string $value): string 132 | { 133 | $value = str_replace(self::$foundEntitiesCache[0], self::$foundEntitiesCache[1], $value); 134 | if (strstr($value, 'html5-dom-document-internal-entity') !== false) { 135 | $search = []; 136 | $replace = []; 137 | $matches = []; 138 | preg_match_all('/html5-dom-document-internal-entity([12])-(.*?)-end/', $value, $matches); 139 | $matches[0] = array_unique($matches[0]); 140 | foreach ($matches[0] as $i => $match) { 141 | $search[] = $match; 142 | $replace[] = html_entity_decode(($matches[1][$i] === '1' ? '&' : '&#') . $matches[2][$i] . ';'); 143 | } 144 | $value = str_replace($search, $replace, $value); 145 | self::$foundEntitiesCache[0] = array_merge(self::$foundEntitiesCache[0], $search); 146 | self::$foundEntitiesCache[1] = array_merge(self::$foundEntitiesCache[1], $replace); 147 | unset($search); 148 | unset($replace); 149 | unset($matches); 150 | } 151 | return $value; 152 | } 153 | 154 | /** 155 | * Returns the updated nodeValue Property 156 | * 157 | * @return string The updated $nodeValue 158 | */ 159 | public function getNodeValue(): string 160 | { 161 | return $this->updateResult($this->nodeValue); 162 | } 163 | 164 | /** 165 | * Returns the updated $textContent Property 166 | * 167 | * @return string The updated $textContent 168 | */ 169 | public function getTextContent(): string 170 | { 171 | return $this->updateResult($this->textContent); 172 | } 173 | 174 | /** 175 | * Returns the value for the attribute name specified. 176 | * 177 | * @param string $name The attribute name. 178 | * @return string The attribute value. 179 | * @throws \InvalidArgumentException 180 | */ 181 | public function getAttribute($name): string 182 | { 183 | if ($this->attributes->length === 0) { // Performance optimization 184 | return ''; 185 | } 186 | $value = parent::getAttribute($name); 187 | return $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : ''; 188 | } 189 | 190 | /** 191 | * Returns an array containing all attributes. 192 | * 193 | * @return array An associative array containing all attributes. 194 | */ 195 | public function getAttributes(): array 196 | { 197 | $attributes = []; 198 | foreach ($this->attributes as $attributeName => $attribute) { 199 | $value = $attribute->value; 200 | $attributes[$attributeName] = $value !== '' ? (strstr($value, 'html5-dom-document-internal-entity') !== false ? $this->updateResult($value) : $value) : ''; 201 | } 202 | return $attributes; 203 | } 204 | 205 | /** 206 | * Returns the element outerHTML. 207 | * 208 | * @return string The element outerHTML. 209 | */ 210 | public function __toString(): string 211 | { 212 | return $this->outerHTML; 213 | } 214 | 215 | /** 216 | * Returns the first child element matching the selector. 217 | * 218 | * @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul. 219 | * @return HTML5DOMElement|null The result DOMElement or null if not found. 220 | * @throws \InvalidArgumentException 221 | */ 222 | public function querySelector(string $selector) 223 | { 224 | return $this->internalQuerySelector($selector); 225 | } 226 | 227 | /** 228 | * Returns a list of children elements matching the selector. 229 | * 230 | * @param string $selector A CSS query selector. Available values: *, tagname, tagname#id, #id, tagname.classname, .classname, tagname.classname.classname2, .classname.classname2, tagname[attribute-selector], [attribute-selector], "div, p", div p, div > p, div + p and p ~ ul. 231 | * @return HTML5DOMNodeList Returns a list of DOMElements matching the criteria. 232 | * @throws \InvalidArgumentException 233 | */ 234 | public function querySelectorAll(string $selector) 235 | { 236 | return $this->internalQuerySelectorAll($selector); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /resources/views/css/theme.phtml: -------------------------------------------------------------------------------- 1 | /* ---------------------------------------------------------------------------- */ 2 | /* ---------------------------------------------------------------------------- */ 3 | /* ---------------------------------------------------------------------------- */ 4 | 5 | /* 6 | adjustments as suggested here 7 | https://www.webtrees.net/index.php/en/forum/3-help-for-2-0-alpha/32882-solved-support-for-bigger-monitors#70135 8 | */ 9 | 10 | getPreference('FULL_WIDTH', '1'))) : ?> 11 | 12 | @media (min-width:576px){.container-lg{max-width:540px}} /* bootstrap: col-sm */ 13 | @media (min-width:768px){.container-lg{max-width:720px}} /* bootstrap: col-md */ 14 | @media (min-width:900px){.container-lg{max-width:860px}} /* extra step added */ 15 | @media (min-width:992px){.container-lg{max-width:960px}} /* extra step added, bootstrap: col-lg */ 16 | @media (min-width:1024px){.container-lg{max-width:974px}} 17 | @media (min-width:1200px){.container-lg{max-width:1140px}} /* extra step added, bootstrap: col-xl */ 18 | @media (min-width:1280px){.container-lg{max-width:1220px}} 19 | @media (min-width:1400px){.container-lg{max-width:1340px}} 20 | @media (min-width:1600px){.container-lg{max-width:1540px}} 21 | @media (min-width:1680px){.container-lg{max-width:1620px}} 22 | @media (min-width:1920px){.container-lg{max-width:1860px}} 23 | @media (min-width:2560px){.container-lg{max-width:2500px}} 24 | 25 | 26 | 27 | /* edit containers do not have to be that wide though on larger screens */ 28 | @media (min-width:576px){.edit-container{max-width:540px}} 29 | @media (min-width:768px){.edit-container{max-width:720px}} 30 | @media (min-width:1024px){.edit-container{max-width:720px}} 31 | @media (min-width:1280px){.edit-container{max-width:720px}} 32 | @media (min-width:1400px){.edit-container{max-width:720px}} 33 | @media (min-width:1600px){.edit-container{max-width:720px}} 34 | @media (min-width:1680px){.edit-container{max-width:720px}} 35 | @media (min-width:1920px){.edit-container{max-width:720px}} 36 | @media (min-width:2560px){.edit-container{max-width:720px}} 37 | 38 | /* align with edit-container */ 39 | @media (min-width:576px){.container.card.wt-osk{max-width:540px}} 40 | @media (min-width:768px){.container.card.wt-osk{max-width:720px}} 41 | @media (min-width:1024px){.container.card.wt-osk{max-width:720px}} 42 | @media (min-width:1280px){.container.card.wt-osk{max-width:720px}} 43 | @media (min-width:1400px){.container.card.wt-osk{max-width:720px}} 44 | @media (min-width:1600px){.container.card.wt-osk{max-width:720px}} 45 | @media (min-width:1680px){.container.card.wt-osk{max-width:720px}} 46 | @media (min-width:1920px){.container.card.wt-osk{max-width:720px}} 47 | @media (min-width:2560px){.container.card.wt-osk{max-width:720px}} 48 | 49 | /* also do not center them? 50 | [dir="ltr"] .edit-container, 51 | [dir="rtl"] .edit-container { 52 | padding-right: 15px; 53 | padding-left: 15px; 54 | margin-right: 15px; 55 | margin-left: 15px; 56 | //original values: 57 | //padding-right: 15px; 58 | //padding-left: 15px; 59 | //margin-right: auto; 60 | //margin-left: auto; 61 | } 62 | */ 63 | 64 | /* ---------------------------------------------------------------------------- */ 65 | /* ---------------------------------------------------------------------------- */ 66 | /* ---------------------------------------------------------------------------- */ 67 | 68 | getPreference('COMPACT_INDI_PAGE', '1')) > 0) : ?> 69 | 70 | /* 71 | individual: title 72 | */ 73 | 74 | [dir] .mb-4, [dir] .my-4 { 75 | margin-bottom: 0.25rem !important; 76 | /* original values: 77 | margin-bottom: 1.5rem !important; 78 | */ 79 | } 80 | 81 | /* 82 | individual: name parts 83 | IndividualController.formatNameRecord 84 | */ 85 | 86 | [dir] .btn-link { 87 | padding: .125rem .125rem; 88 | /* original values: 89 | padding: .25rem .25rem; 90 | */ 91 | } 92 | 93 | [dir] .card-header { 94 | padding: .25rem .75rem; 95 | /* original values: 96 | padding: .75rem 1.25rem; 97 | */ 98 | } 99 | 100 | [dir] .card-body { 101 | padding: .25rem .75rem; 102 | /* original values: 103 | padding: 1.25rem; 104 | */ 105 | } 106 | 107 | [dir] dl { 108 | margin-bottom: 0rem; 109 | /* original values: 110 | margin-bottom: 1rem; 111 | */ 112 | } 113 | 114 | dl dd { 115 | display: inline; 116 | } 117 | 118 | dl dd:after{ 119 | display: block; 120 | content: ''; 121 | } 122 | 123 | dl dt{ 124 | display: inline-block; 125 | margin-right: 2ch; 126 | } 127 | 128 | /* 129 | everywhere: do not use sex icon images 130 | */ 131 | 132 | .wt-icon-sex-m { 133 | content: none; 134 | color: #31639c; 135 | } 136 | 137 | .wt-icon-sex-f { 138 | content: none; 139 | color: #9c3163; 140 | } 141 | 142 | .wt-icon-sex-u { 143 | content: none; 144 | } 145 | 146 | /* 147 | individual: sex icon 148 | */ 149 | 150 | .vt_gender .wt-icon-sex-m,.vt_gender .wt-icon-sex-f,.vt_gender .wt-icon-sex-u { 151 | width: 0.8em; 152 | height: 0.8em; 153 | font-size: 1.5rem !important; 154 | } 155 | 156 | /* 157 | individual: tabs 158 | */ 159 | 160 | [dir] .pt-4, [dir] .py-4 { 161 | padding-top: .25rem !important; 162 | padding-bottom: .25rem !important; 163 | /* original values: 164 | padding-top: 1.5rem !important; 165 | padding-bottom: 1.5rem !important; 166 | */ 167 | } 168 | 169 | [dir] .wt-facts-table { 170 | margin-bottom: 0rem; 171 | /* original values: 172 | margin-bottom: 1rem; 173 | */ 174 | } 175 | 176 | [dir] .wt-facts-table td, [dir] .table th { 177 | padding: .25rem; 178 | /* original values: 179 | padding: .75rem; 180 | */ 181 | } 182 | 183 | [dir] label { 184 | margin-bottom: 0rem; 185 | /* original values: 186 | margin-bottom: .5rem; 187 | */ 188 | } 189 | 190 | [dir] caption { 191 | padding-top: .25rem; 192 | padding-bottom: .25rem; 193 | /* original values: 194 | padding-top: .75rem; 195 | padding-bottom: .75rem; 196 | */ 197 | } 198 | 199 | 200 | 201 | /* ---------------------------------------------------------------------------- */ 202 | /* ---------------------------------------------------------------------------- */ 203 | /* ---------------------------------------------------------------------------- */ 204 | 205 | /* 206 | Family navigator sidebar 207 | 208 | make long names, small display sizes etc behave better - all this should probably be in webtrees itself! 209 | (we have made this part of the layout smaller, so it's even more important here though) 210 | */ 211 | 212 | [dir] .wt-family-navigator-family { 213 | table-layout: fixed; 214 | } 215 | 216 | [dir] .wt-family-navigator-family th { 217 | /* effectively affects the first column. Can we set a more flexible width? apparently not with table layout 'fixed' */ 218 | width: 30%; 219 | 220 | /* [2022/08] had to diable this because it affects the dropdown menu, apparently not possible to solve this without additional wrapper element? */ 221 | /* overflow: hidden; */ 222 | } 223 | 224 | [dir] .wt-family-navigator-name { 225 | overflow: hidden; 226 | } 227 | 228 | [dir] .wt-family-navigator-child > td { 229 | overflow: hidden; 230 | } 231 | 232 | [dir] .wt-family-navigator-family .dropdown-toggle { 233 | /* 234 | not sure what original 'nowrap' is supposed to accomplish, 235 | effect is: 'younger sister' wraps, 'younger sister (dropdown arrow)' doesn't. That's not what we want! 236 | probably intended to prevent wrapping of the dropdown arrow (actually some borders, WTF - this is from bootstrap), 237 | which itself is added via css (with implicit whitespace wrt the white-space property, apparently) 238 | */ 239 | white-space: normal; 240 | } 241 | 242 | /* note: very long names in dropdown are still problematic */ 243 | [dir] .wt-family-navigator-family .dropdown-item { 244 | white-space: normal; 245 | } 246 | 247 | /* ---------------------------------------------------------------------------- */ 248 | /* ---------------------------------------------------------------------------- */ 249 | /* ---------------------------------------------------------------------------- */ 250 | 251 | /* 252 | edit dialogs 253 | */ 254 | 255 | /* 256 | more space for names (should use this regardless of compact layout?) 257 | also pad and border etc like a form-control 258 | (maybe just use class form-control?) 259 | */ 260 | .edit-container input.NAME, .edit-container input._MARNM { 261 | width: calc(100% - 25px); 262 | 263 | height: calc(1.5em + 2px); 264 | 265 | padding-top: 0.1rem; 266 | padding-right: 0.5rem; 267 | padding-bottom: 0.1rem; 268 | padding-left: 0.5rem; 269 | 270 | background-color: #fff; 271 | background-clip: padding-box; 272 | border: 1px solid #ced4da; 273 | border-radius: .25rem; 274 | } 275 | 276 | /* 277 | compact layout, ok foe webtrees 2.1 278 | */ 279 | 280 | /* all rows */ 281 | .edit-container .row.mb-3 { 282 | margin-bottom: 0.25rem !important; 283 | /* original value: 284 | margin-bottom: 1rem !important; 285 | */ 286 | } 287 | 288 | /* e.g. date input field */ 289 | .edit-container .form-control { 290 | height: calc(1.5em + 2px); 291 | /* original values: 292 | height: calc(1.5em + .75rem + 2px); 293 | */ 294 | } 295 | 296 | /* icons for location, help etc */ 297 | .edit-container .input-group-text { 298 | padding-top: 0rem; 299 | padding-right: 0.75rem; 300 | padding-bottom: 0rem; 301 | padding-left: 0.75rem; 302 | /* original values: 303 | padding-top: 0.375rem; 304 | padding-right: 0.75rem; 305 | padding-bottom: 0.375rem; 306 | padding-left: 0.75rem; 307 | */ 308 | } 309 | 310 | 311 | .edit-container textarea.form-control { 312 | height: auto; 313 | } 314 | 315 | .edit-container .col-form-label { 316 | padding-top: calc(.1rem + 1px); 317 | padding-bottom: calc(.1rem + 1px); 318 | /* original values: 319 | padding-top: calc(.375rem + 1px); 320 | padding-bottom: calc(.375rem + 1px); 321 | */ 322 | margin-bottom: 0; 323 | } 324 | 325 | /* ---------------------------------------------------------------------------- */ 326 | /* ---------------------------------------------------------------------------- */ 327 | /* ---------------------------------------------------------------------------- */ 328 | 329 | /* issue #50 */ 330 | /* Add space between thumbnails */ 331 | .p-1 .gallery { 332 | padding-left: 2px; 333 | } 334 | -------------------------------------------------------------------------------- /patchedWebtrees/IndividualExt.php: -------------------------------------------------------------------------------- 1 | settings; 19 | } 20 | 21 | public function __construct( 22 | string $xref, 23 | string $gedcom, 24 | ?string $pending, 25 | Tree $tree, 26 | IndividualExtSettings $settings) 27 | { 28 | parent::__construct($xref, $gedcom, $pending, $tree); 29 | 30 | $this->settings = $settings; 31 | } 32 | 33 | /** 34 | * Convert a name record into ‘full’ and ‘sort’ versions. 35 | * Use the NAME field to generate the ‘full’ version, as the 36 | * gedcom spec says that this is the individual’s name, as they would write it. 37 | * Use the SURN field to generate the sortable names. Note that this field 38 | * may also be used for the ‘true’ surname, perhaps spelt differently to that 39 | * recorded in the NAME field. e.g. 40 | * 41 | * 1 NAME Robert /de Gliderow/ 42 | * 2 GIVN Robert 43 | * 2 SPFX de 44 | * 2 SURN CLITHEROW 45 | * 2 NICK The Bald 46 | * 47 | * full=>'Robert de Gliderow 'The Bald'' 48 | * sort=>'CLITHEROW, ROBERT' 49 | * 50 | * Handle multiple surnames, either as; 51 | * 52 | * 1 NAME Carlos /Vasquez/ y /Sante/ 53 | * or 54 | * 1 NAME Carlos /Vasquez y Sante/ 55 | * 2 GIVN Carlos 56 | * 2 SURN Vasquez,Sante 57 | * 58 | * @param string $type 59 | * @param string $full 60 | * @param string $gedcom 61 | * 62 | * @return void 63 | */ 64 | protected function addName(string $type, string $full, string $gedcom): void 65 | { 66 | //////////////////////////////////////////////////////////////////////////// 67 | // Extract the structured name parts - use for "sortable" names and indexes 68 | //////////////////////////////////////////////////////////////////////////// 69 | 70 | $sublevel = 1 + (int) substr($gedcom, 0, 1); 71 | $GIVN = preg_match("/\n{$sublevel} GIVN (.+)/", $gedcom, $match) ? $match[1] : ''; 72 | $SURN = preg_match("/\n{$sublevel} SURN (.+)/", $gedcom, $match) ? $match[1] : ''; 73 | $NICK = preg_match("/\n{$sublevel} NICK (.+)/", $gedcom, $match) ? $match[1] : ''; 74 | 75 | // SURN is an comma-separated list of surnames... 76 | if ($SURN !== '') { 77 | $SURNS = preg_split('/ *, */', $SURN); 78 | } else { 79 | $SURNS = []; 80 | } 81 | 82 | // ...so is GIVN - but nobody uses it like that 83 | $GIVN = str_replace('/ *, */', ' ', $GIVN); 84 | 85 | //////////////////////////////////////////////////////////////////////////// 86 | // Extract the components from NAME - use for the "full" names 87 | //////////////////////////////////////////////////////////////////////////// 88 | 89 | // Fix bad slashes. e.g. 'John/Smith' => 'John/Smith/' 90 | if (substr_count($full, '/') % 2 === 1) { 91 | $full .= '/'; 92 | } 93 | 94 | // GEDCOM uses "//" to indicate an unknown surname 95 | $full = preg_replace('/\/\//', '/@N.N./', $full); 96 | 97 | // Extract the surname. 98 | // Note, there may be multiple surnames, e.g. Jean /Vasquez/ y /Cortes/ 99 | if (preg_match('/\/.*\//', $full, $match)) { 100 | $surname = str_replace('/', '', $match[0]); 101 | } else { 102 | $surname = ''; 103 | } 104 | 105 | // If we don’t have a SURN record, extract it from the NAME 106 | if (!$SURNS) { 107 | if (preg_match_all('/\/([^\/]*)\//', $full, $matches)) { 108 | // There can be many surnames, each wrapped with '/' 109 | $SURNS = $matches[1]; 110 | foreach ($SURNS as $n => $SURN) { 111 | // Remove surname prefixes, such as "van de ", "d'" and "'t " (lower case only) 112 | $SURNS[$n] = preg_replace('/^(?:[a-z]+ |[a-z]+\' ?|\'[a-z]+ )+/', '', $SURN); 113 | } 114 | } else { 115 | // It is valid not to have a surname at all 116 | $SURNS = ['']; 117 | } 118 | } 119 | 120 | // If we don’t have a GIVN record, extract it from the NAME 121 | if (!$GIVN) { 122 | $GIVN = preg_replace( 123 | [ 124 | '/ ?\/.*\/ ?/', 125 | // remove surname 126 | '/ ?".+"/', 127 | // remove nickname 128 | '/ {2,}/', 129 | // multiple spaces, caused by the above 130 | '/^ | $/', 131 | // leading/trailing spaces, caused by the above 132 | ], 133 | [ 134 | ' ', 135 | ' ', 136 | ' ', 137 | '', 138 | ], 139 | $full 140 | ); 141 | } 142 | 143 | // Add placeholder for unknown given name 144 | if (!$GIVN) { 145 | $GIVN = self::PRAENOMEN_NESCIO; 146 | $pos = (int) strpos($full, '/'); 147 | $full = substr($full, 0, $pos) . '@P.N. ' . substr($full, $pos); 148 | } 149 | 150 | //[RC] adjusted 151 | $fullForFullNN = $full; 152 | 153 | //Issue #115 154 | //slashes in $full have special meaning, must use something else (at least for creating the html) 155 | //should we revert this for display afterwards? 156 | // not sure, if nickname is set as part of full by user, he wouldn't be able to use a slash either 157 | //(anyway there is nothing in the spec about this case) 158 | $NICK = str_replace('/', '|', $NICK); 159 | 160 | //[RC] adjusted: logic is configurable 161 | if ($NICK && strpos($full, '"' . $NICK . '"') === false) { 162 | // A NICK field is present, but not included in the NAME. 163 | // we may have to handle it specifically 164 | $handler = \Vesta\VestaUtils::get(IndividualNameHandler::class); 165 | $full = $handler->addNick($full, $NICK); 166 | } 167 | 168 | //moved to fullName(): we don't want this e.g. when using individual name for family name 169 | //$full = $handler->addXref($full, $this->xref()); 170 | 171 | // Remove slashes - they don’t get displayed 172 | // $fullNN keeps the @N.N. placeholders, for the database 173 | // $full is for display on-screen 174 | $fullNN = str_replace('/', '', $fullForFullNN); 175 | 176 | // Insert placeholders for any missing/unknown names 177 | $full = str_replace(self::NOMEN_NESCIO, I18N::translateContext('Unknown surname', '…'), $full); 178 | $full = str_replace(self::PRAENOMEN_NESCIO, I18N::translateContext('Unknown given name', '…'), $full); 179 | // Format for display 180 | $full = '' . preg_replace('/\/([^\/]*)\//', '$1', e($full)) . ''; 181 | // Localise quotation marks around the nickname 182 | $full = preg_replace_callback('/"([^&]*)"/', static function (array $matches): string { 183 | return '' . $matches[1] . ''; 184 | }, $full); 185 | 186 | // A suffix of “*” indicates a preferred name 187 | //[RC] this was originally adjusted, rationale: '-' is a breaking-space character, therefore break preferred name as well (this is to handle names with hyphens where only the last part is the preferred name) 188 | //(although note that 'official' german rufname must always be full hyphenated name) 189 | //explicitly use unicode 2011 'Non-Breaking Hyphen' if intended otherwise 190 | // 191 | //note: webtrees has different solution for this since fix for https://github.com/fisharebest/webtrees/issues/1010 192 | //Issue #76: we'll just align with that, everything else gets too complicated 193 | //(we would have to add config option whether to break on breaking-space characters) 194 | //anyway the more common case seems to be the one where you want all parts as preferred name 195 | $full = preg_replace('/([^ >\x{200C}]*)\*/u', '\\1', $full); 196 | 197 | // Remove prefered-name indicater - they don’t go in the database 198 | $GIVN = str_replace('*', '', $GIVN); 199 | $fullNN = str_replace('*', '', $fullNN); 200 | 201 | foreach ($SURNS as $SURN) { 202 | // Scottish 'Mc and Mac ' prefixes both sort under 'Mac' 203 | if (strcasecmp(substr($SURN, 0, 2), 'Mc') === 0) { 204 | $SURN = substr_replace($SURN, 'Mac', 0, 2); 205 | } elseif (strcasecmp(substr($SURN, 0, 4), 'Mac ') === 0) { 206 | $SURN = substr_replace($SURN, 'Mac', 0, 4); 207 | } 208 | 209 | $this->getAllNames[] = [ 210 | 'type' => $type, 211 | 'sort' => $SURN . ',' . $GIVN, 212 | 'full' => $full, 213 | // This is used for display 214 | 'fullNN' => $fullNN, 215 | // This goes into the database 216 | 'surname' => $surname, 217 | // This goes into the database 218 | 'givn' => $GIVN, 219 | // This goes into the database 220 | 'surn' => $SURN, 221 | // This goes into the database 222 | ]; 223 | } 224 | } 225 | 226 | public function fullName(): string 227 | { 228 | //[RC] adjusted: logic is configurable 229 | $handler = \Vesta\VestaUtils::get(IndividualNameHandler::class); 230 | 231 | $full = parent::fullName(); 232 | $full = $handler->addBadges($full, $this->tree(), $this->gedcom); 233 | $full = $handler->addXref($full, $this->xref()); 234 | return $full; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "version": "2.0.13.0.0", 4 | "from": "2.0.12", 5 | "to": "2.0.18" 6 | }, 7 | { 8 | "version": "2.0.13.1.0", 9 | "from": "2.0.12", 10 | "to": "2.0.18", 11 | "changelog": ["Handling of nicknames aligned with main webtrees."] 12 | }, 13 | { 14 | "version": "2.0.15.0.0", 15 | "from": "2.0.12", 16 | "to": "2.0.18" 17 | }, 18 | { 19 | "version": "2.0.15.1.0", 20 | "from": "2.0.12", 21 | "to": "2.0.18" 22 | }, 23 | { 24 | "version": "2.0.15.2.0", 25 | "from": "2.0.12", 26 | "to": "2.0.18" 27 | }, 28 | { 29 | "version": "2.0.15.3.0", 30 | "from": "2.0.12", 31 | "to": "2.0.18", 32 | "changelog": ["Bugfix: Handle old libxml versions."] 33 | }, 34 | { 35 | "version": "2.0.15.4.0", 36 | "from": "2.0.12", 37 | "to": "2.0.18" 38 | }, 39 | { 40 | "version": "2.0.16.0.0", 41 | "from": "2.0.12", 42 | "to": "2.0.18" 43 | }, 44 | { 45 | "version": "2.0.16.0.1", 46 | "from": "2.0.12", 47 | "to": "2.0.18", 48 | "changelog": ["Small layout fix."] 49 | }, 50 | { 51 | "version": "2.0.16.0.3", 52 | "from": "2.0.12", 53 | "to": "2.0.18", 54 | "changelog": ["Small layout fix."] 55 | }, 56 | { 57 | "version": "2.0.16.0.4", 58 | "from": "2.0.12", 59 | "to": "2.0.18", 60 | "changelog": ["Small layout fix."] 61 | }, 62 | { 63 | "version": "2.0.16.1.0", 64 | "from": "2.0.12", 65 | "to": "2.0.18" 66 | }, 67 | { 68 | "version": "2.0.16.2.0", 69 | "from": "2.0.12", 70 | "to": "2.0.18" 71 | }, 72 | { 73 | "version": "2.0.16.3.0", 74 | "from": "2.0.12", 75 | "to": "2.0.18" 76 | }, 77 | { 78 | "version": "2.0.16.4.0", 79 | "from": "2.0.12", 80 | "to": "2.0.18" 81 | }, 82 | { 83 | "version": "2.0.16.5.0", 84 | "from": "2.0.12", 85 | "to": "2.0.18" 86 | }, 87 | { 88 | "version": "2.0.16.6.0", 89 | "from": "2.0.12", 90 | "to": "2.0.18" 91 | }, 92 | { 93 | "version": "2.0.17.0.0", 94 | "from": "2.0.12", 95 | "to": "2.0.18" 96 | }, 97 | { 98 | "version": "2.0.17.0.1", 99 | "from": "2.0.12", 100 | "to": "2.0.18" 101 | }, 102 | { 103 | "version": "2.0.17.1.0", 104 | "from": "2.0.12", 105 | "to": "2.0.18" 106 | }, 107 | { 108 | "version": "2.0.19.0.0", 109 | "from": "2.0.12", 110 | "to": "2.0.20", 111 | "changelog": ["Add option to preserve GEDCOM linebreaks in markdown formatted text."] 112 | }, 113 | { 114 | "version": "2.0.19.0.1", 115 | "from": "2.0.12", 116 | "to": "2.0.20", 117 | "changelog": ["Small bugfix for option introduced in 2.0.19.0.0."] 118 | }, 119 | { 120 | "version": "2.0.19.0.2", 121 | "from": "2.0.12", 122 | "to": "2.0.20", 123 | "changelog": ["Another bugfix for option introduced in 2.0.19.0.0."] 124 | }, 125 | { 126 | "version": "2.0.19.0.3", 127 | "from": "2.0.12", 128 | "to": "2.0.20", 129 | "changelog": ["Another bugfix for option introduced in 2.0.19.0.0."] 130 | }, 131 | { 132 | "version": "2.0.19.1.0", 133 | "from": "2.0.12", 134 | "to": "2.0.20" 135 | }, 136 | { 137 | "version": "2.0.19.2.0", 138 | "from": "2.0.12", 139 | "to": "2.0.20" 140 | }, 141 | { 142 | "version": "2.0.22.0.0", 143 | "from": "2.0.12", 144 | "to": "2.0.23" 145 | }, 146 | { 147 | "version": "2.0.23+2.1.0-beta.2.0.0", 148 | "from": "2.0.12", 149 | "to": "2.1.0" 150 | }, 151 | { 152 | "version": "2.0.23+2.1.0-beta.2.0.1", 153 | "from": "2.0.12", 154 | "to": "2.1.0" 155 | }, 156 | { 157 | "version": "2.0.23+2.1.0-beta.2.1.0", 158 | "from": "2.0.12", 159 | "to": "2.1.0" 160 | }, 161 | { 162 | "version": "2.0.23+2.1.0-beta.2.2.0", 163 | "from": "2.0.12", 164 | "to": "2.1.0" 165 | }, 166 | { 167 | "version": "2.0.23+2.1.0-beta.2.3.0", 168 | "from": "2.0.12", 169 | "to": "2.1.0" 170 | }, 171 | { 172 | "version": "2.1.0.0.0", 173 | "from": "2.0.12", 174 | "to": "2.1.1" 175 | }, 176 | { 177 | "version": "2.1.0.1.0", 178 | "from": "2.0.12", 179 | "to": "2.1.1" 180 | }, 181 | { 182 | "version": "2.1.1.0.0", 183 | "from": "2.0.12", 184 | "to": "2.1.2" 185 | }, 186 | { 187 | "version": "2.1.2.0.0", 188 | "from": "2.0.12", 189 | "to": "2.1.3" 190 | }, 191 | { 192 | "version": "2.1.2.1.0", 193 | "from": "2.0.12", 194 | "to": "2.1.3" 195 | }, 196 | { 197 | "version": "2.1.2.1.1", 198 | "from": "2.0.12", 199 | "to": "2.1.3" 200 | }, 201 | { 202 | "version": "2.1.2.1.2", 203 | "from": "2.0.12", 204 | "to": "2.1.3" 205 | }, 206 | { 207 | "version": "2.1.4.0.0", 208 | "from": "2.1.4", 209 | "to": "2.1.6" 210 | }, 211 | { 212 | "version": "2.1.4.1.0", 213 | "from": "2.1.4", 214 | "to": "2.1.6" 215 | }, 216 | { 217 | "version": "2.1.5.0.0", 218 | "from": "2.1.4", 219 | "to": "2.1.6", 220 | "changelog": ["Bugfix for JustLight theme integration."] 221 | }, 222 | { 223 | "version": "2.1.6.0.0", 224 | "from": "2.1.4", 225 | "to": "2.1.7", 226 | "changelog": ["New config option for name types when adding new individuals."] 227 | }, 228 | { 229 | "version": "2.1.6.1.0", 230 | "from": "2.1.4", 231 | "to": "2.1.7", 232 | "changelog": ["Preview: Advanced configuration of GEDCOM tags per event."] 233 | }, 234 | { 235 | "version": "2.1.6.1.1", 236 | "from": "2.1.4", 237 | "to": "2.1.7" 238 | }, 239 | { 240 | "version": "2.1.7.0.0", 241 | "from": "2.1.4", 242 | "to": "2.1.8" 243 | }, 244 | { 245 | "version": "2.1.7.0.1", 246 | "from": "2.1.4", 247 | "to": "2.1.8", 248 | "changelog": ["Bugfix (individual image display)."] 249 | }, 250 | { 251 | "version": "2.1.7.0.2", 252 | "from": "2.1.4", 253 | "to": "2.1.8" 254 | }, 255 | { 256 | "version": "2.1.7.1.0", 257 | "from": "2.1.4", 258 | "to": "2.1.8" 259 | }, 260 | { 261 | "version": "2.1.7.1.1", 262 | "from": "2.1.4", 263 | "to": "2.1.8" 264 | }, 265 | { 266 | "version": "2.1.8.0.0", 267 | "from": "2.1.8", 268 | "to": "2.1.13" 269 | }, 270 | { 271 | "version": "2.1.8.0.1", 272 | "from": "2.1.8", 273 | "to": "2.1.13" 274 | }, 275 | { 276 | "version": "2.1.8.0.2", 277 | "from": "2.1.8", 278 | "to": "2.1.13" 279 | }, 280 | { 281 | "version": "2.1.9.0.0", 282 | "from": "2.1.8", 283 | "to": "2.1.13" 284 | }, 285 | { 286 | "version": "2.1.9.1.0", 287 | "from": "2.1.8", 288 | "to": "2.1.13" 289 | }, 290 | { 291 | "version": "2.1.9.9.9", 292 | "from": "2.1.10", 293 | "to": "2.1.13" 294 | }, 295 | { 296 | "version": "2.1.10.0.0", 297 | "from": "2.1.10", 298 | "to": "2.1.13" 299 | }, 300 | { 301 | "version": "2.1.13.0.0", 302 | "from": "2.1.10", 303 | "to": "2.1.13" 304 | }, 305 | { 306 | "version": "2.1.13.0.2", 307 | "from": "2.1.13", 308 | "to": "2.1.14" 309 | }, 310 | { 311 | "version": "2.1.15.0.0", 312 | "from": "2.1.15", 313 | "to": "2.1.17" 314 | }, 315 | { 316 | "version": "2.1.15.0.1", 317 | "from": "2.1.15", 318 | "to": "2.1.17" 319 | }, 320 | { 321 | "version": "2.1.16.0.0", 322 | "from": "2.1.15", 323 | "to": "2.1.17" 324 | }, 325 | { 326 | "version": "2.1.16.1.0", 327 | "from": "2.1.15", 328 | "to": "2.1.17", 329 | "changelog": ["New config option for initial display of name blocks on individual page."] 330 | }, 331 | { 332 | "version": "2.1.16.1.1", 333 | "from": "2.1.15", 334 | "to": "2.1.17" 335 | }, 336 | { 337 | "version": "2.1.16.1.3", 338 | "from": "2.1.15", 339 | "to": "2.1.17", 340 | "changelog": ["New menu entry for individuals 'Edit main facts and events' (edit multiple facts at once, similar to functionality for new individuals)."] 341 | }, 342 | { 343 | "version": "2.1.16.1.4", 344 | "from": "2.1.15", 345 | "to": "2.1.17" 346 | }, 347 | { 348 | "version": "2.1.16.1.5", 349 | "from": "2.1.15", 350 | "to": "2.1.17" 351 | }, 352 | { 353 | "version": "2.1.16.2.0", 354 | "from": "2.1.15", 355 | "to": "2.1.17" 356 | }, 357 | { 358 | "version": "2.1.17.0.0", 359 | "from": "2.1.17", 360 | "to": "2.1.18" 361 | }, 362 | { 363 | "version": "2.1.17.1.0", 364 | "from": "2.1.17", 365 | "to": "2.1.18" 366 | }, 367 | { 368 | "version": "2.1.18.0.0", 369 | "from": "2.1.17", 370 | "to": "2.1.21" 371 | }, 372 | { 373 | "version": "2.1.18.0.1", 374 | "from": "2.1.17", 375 | "to": "2.1.21" 376 | }, 377 | { 378 | "version": "2.1.18.1.0", 379 | "from": "2.1.17", 380 | "to": "2.1.21" 381 | }, 382 | { 383 | "version": "2.1.18.2.0", 384 | "from": "2.1.17", 385 | "to": "2.1.21" 386 | }, 387 | { 388 | "version": "2.1.18.2.1", 389 | "from": "2.1.17", 390 | "to": "2.1.21", 391 | "changelog": ["New feature 'Name badges'."] 392 | }, 393 | { 394 | "version": "2.1.18.2.2", 395 | "from": "2.1.17", 396 | "to": "2.1.21" 397 | }, 398 | { 399 | "version": "2.1.19.0.0", 400 | "from": "2.1.17", 401 | "to": "2.1.21" 402 | }, 403 | { 404 | "version": "2.1.20.0.0", 405 | "from": "2.1.17", 406 | "to": "2.1.21" 407 | }, 408 | { 409 | "version": "2.1.20.1.0", 410 | "from": "2.1.17", 411 | "to": "2.1.21", 412 | "changelog": ["Fixed integration with 'JustLight' theme."] 413 | }, 414 | { 415 | "version": "2.1.20.1.2", 416 | "from": "2.1.17", 417 | "to": "2.1.21", 418 | "changelog": ["Fixed integration with 'JustLight' theme (from version 2.2.8)."] 419 | }, 420 | { 421 | "version": "2.1.20.2.0", 422 | "from": "2.1.17", 423 | "to": "2.1.21" 424 | }, 425 | { 426 | "version": "2.2.0.0.0", 427 | "from": "2.1.17", 428 | "to": "2.2.1" 429 | }, 430 | { 431 | "version": "2.2.1.0.0", 432 | "from": "2.1.17", 433 | "to": "2.2.2" 434 | }, 435 | { 436 | "version": "2.2.1.1.0", 437 | "from": "2.1.17", 438 | "to": "2.2.2" 439 | }, 440 | { 441 | "version": "2.2.1.2.0", 442 | "from": "2.2.1", 443 | "to": "2.2.2" 444 | }, 445 | { 446 | "version": "2.2.1.2.1", 447 | "from": "2.2.1", 448 | "to": "2.2.2", 449 | "changelog": ["Support back reference in name badges."] 450 | }, 451 | { 452 | "version": "2.2.1.3.0", 453 | "from": "2.2.1", 454 | "to": "2.2.2" 455 | }, 456 | { 457 | "version": "2.2.1.3.1", 458 | "from": "2.2.1", 459 | "to": "2.2.2" 460 | }, 461 | { 462 | "version": "2.2.1.4.0", 463 | "from": "2.2.1", 464 | "to": "2.2.2", 465 | "changelog": ["Added option to display specific name badges in front of names."] 466 | }, 467 | { 468 | "version": "2.2.1.4.1", 469 | "from": "2.2.1", 470 | "to": "2.2.2", 471 | "changelog": ["Fixed name badges display order."] 472 | }, 473 | { 474 | "version": "2.2.1.5.0", 475 | "from": "2.2.1", 476 | "to": "2.2.2", 477 | "changelog": ["Fixed issue #168 (configuration of visibility of GEDCOM tags)."] 478 | }, 479 | { 480 | "version": "2.2.2.0.0", 481 | "from": "2.2.1", 482 | "to": "2.2.4" 483 | }, 484 | { 485 | "version": "2.2.3.0.0", 486 | "from": "2.2.1", 487 | "to": "2.2.4" 488 | }, 489 | { 490 | "version": "2.2.4.0.0", 491 | "from": "2.2.1", 492 | "to": "2.2.5" 493 | }, 494 | { 495 | "version": "2.2.4.1.0", 496 | "from": "2.2.1", 497 | "to": "2.2.5" 498 | } 499 | ] 500 | -------------------------------------------------------------------------------- /resources/views/layouts/default.phtml: -------------------------------------------------------------------------------- 1 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | <?= strip_tags($title) ?> 50 | <?php if ($tree !== null && $tree->getPreference('META_TITLE') !== '') : ?> 51 | – <?= e($tree->getPreference('META_TITLE')) ?> 52 | <?php endif ?> 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | stylesheets() as $stylesheet) : ?> 67 | 68 | 69 | 70 | 71 | 72 | findByInterface(ModuleGlobalInterface::class)->map(static function (ModuleGlobalInterface $module): string { 73 | return $module->headContent(); 74 | })->implode('') ?> 75 | 76 | 77 | 78 | 79 | 82 | 83 | 84 |
85 |
86 |
87 | 93 | 94 | 95 | 96 |

title()) ?>

97 | 98 | 115 | 116 | 117 |
118 | 123 |
124 | 125 | 126 | 133 | 134 |
135 |
136 |
137 | 138 | 139 |
140 | 141 |
142 | 143 |
144 | 145 |
146 | 147 | 152 | 153 |
154 | 155 | 156 |
157 |
158 | 159 | 160 | 161 |
162 | findByInterface(ModuleFooterInterface::class)->map(static function (ModuleFooterInterface $module) use ($request): string { 163 | return $module->getFooter($request); 164 | })->implode('') ?> 165 |
166 | 167 | 168 | 169 | 170 | 171 | 204 | 205 | 206 | 207 | findByInterface(ModuleGlobalInterface::class)->map(static function (ModuleGlobalInterface $module): string { 208 | return $module->bodyContent(); 209 | })->implode('') ?> 210 | 211 | 212 | -------------------------------------------------------------------------------- /patchedWebtrees/Services/GedcomEditServiceExt2.php: -------------------------------------------------------------------------------- 1 | forNewIndividual = $forNewIndividual; 34 | $this->vesta_symbol = json_decode('"\u26B6"'); 35 | $this->house_symbol = json_decode('"\u2302"'); 36 | } 37 | 38 | protected function insertMissingLevels(Tree $tree, string $tag, string $gedcom, bool $include_hidden): string 39 | { 40 | $next_level = substr_count($tag, ':') + 1; 41 | $factory = Registry::elementFactory(); 42 | $subtags = $factory->make($tag)->subtags(); 43 | 44 | // The first part is level N. The remainder are level N+1. 45 | $parts = preg_split('/\n(?=' . $next_level . ')/', $gedcom); 46 | $return = array_shift($parts) ?? ''; 47 | 48 | foreach ($subtags as $subtag => $occurrences) { 49 | if (!$include_hidden) { 50 | 51 | if ("MAP" == $subtag) { 52 | //Issue #168: MAP is special (it has no own edit control) 53 | //hide only if both its subtags are hidden 54 | //(cannot unhide unconditionally: that strategy would show PLAC with empty dropdown if all others are hidden) 55 | $longHidden = $this->isHiddenTagExt( 56 | $tree, 57 | $tag . ':' . $subtag . ':LONG', 58 | false); 59 | 60 | $latiHidden = $this->isHiddenTagExt( 61 | $tree, 62 | $tag . ':' . $subtag . ':LATI', 63 | false); 64 | 65 | $hidden = $longHidden && $latiHidden; 66 | } else { 67 | $hidden = $this->isHiddenTagExt( 68 | $tree, 69 | $tag . ':' . $subtag, 70 | str_ends_with($occurrences, ':?')); 71 | } 72 | 73 | 74 | if ($hidden) { 75 | 76 | //we want to preserve overall order in any case 77 | //(in particular in the 'edit main facts and events' dialog) 78 | //therefore show existing hidden at its 'normal' position, rather than at the end 79 | $existing = false; 80 | foreach ($parts as $n => $part) { 81 | if (str_starts_with($part, $next_level . ' ' . $subtag)) { 82 | $existing = true; 83 | break; 84 | } 85 | } 86 | 87 | if (!$existing) { 88 | continue; 89 | } 90 | } 91 | } 92 | 93 | [$min, $max] = explode(':', $occurrences); 94 | 95 | $min = (int) $min; 96 | 97 | if ($max === 'M') { 98 | $max = PHP_INT_MAX; 99 | } else { 100 | $max = (int) $max; 101 | } 102 | 103 | $count = 0; 104 | 105 | // Add expected subtags in our preferred order. 106 | foreach ($parts as $n => $part) { 107 | if (str_starts_with($part, $next_level . ' ' . $subtag)) { 108 | $return .= "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $part, $include_hidden); 109 | $count++; 110 | unset($parts[$n]); 111 | } 112 | } 113 | 114 | // Allowed to have more of this subtag? 115 | if ($count < $max) { 116 | // Create a new one. 117 | $gedcom = $next_level . ' ' . $subtag; 118 | $default = $factory->make($tag . ':' . $subtag)->default($tree); 119 | if ($default !== '') { 120 | $gedcom .= ' ' . $default; 121 | } 122 | 123 | $number_to_add = max(1, $min - $count); 124 | $gedcom_to_add = "\n" . $this->insertMissingLevels($tree, $tag . ':' . $subtag, $gedcom, $include_hidden); 125 | 126 | $return .= str_repeat($gedcom_to_add, $number_to_add); 127 | } 128 | } 129 | 130 | // Now add any unexpected/existing data. 131 | if ($parts !== []) { 132 | $return .= "\n" . implode("\n", $parts); 133 | } 134 | 135 | return $return; 136 | } 137 | 138 | public function getPreference( 139 | Tree $tree, 140 | string $tag): string { 141 | 142 | $tag = $this->compressTag($tag); 143 | $pref = $this->vesta_symbol . $this->house_symbol . $tag; 144 | $override = $tree->getPreference($pref); 145 | return $override; 146 | } 147 | 148 | public function setPreference( 149 | Tree $tree, 150 | string $tag, 151 | string $value) { 152 | 153 | $tag = $this->compressTag($tag); 154 | $pref = $this->vesta_symbol . $this->house_symbol . $tag; 155 | //TODO would be better to delete pref in case of empty string 156 | $tree->setPreference($pref, $value); 157 | } 158 | 159 | protected function isHiddenTagExt( 160 | Tree $tree, 161 | string $tag, 162 | bool $hiddenViaOccurrences): bool { 163 | 164 | $r = new ReflectionMethod(parent::class, 'isHiddenTag'); 165 | $r->setAccessible(true); 166 | $originalRet = $hiddenViaOccurrences || $r->invokeArgs($this, [$tag]); 167 | 168 | //error_log("hidden? " . $tag . '=' . $originalRet); 169 | 170 | //check for specific override 171 | $tag = $this->compressTag($tag); 172 | $pref = $this->vesta_symbol . $this->house_symbol . $tag; 173 | 174 | //error_log("override? " . $pref); 175 | 176 | $override = $tree->getPreference($pref); 177 | if ($override !== '') { 178 | switch ($override) { 179 | //hide always 180 | case 'LEVEL0': 181 | $overrideRet = true; 182 | break; 183 | //hide for new individual, show otherwise 184 | case 'LEVEL1': 185 | case 'LEVEL1a': 186 | if ($this->forNewIndividual) { 187 | $overrideRet = true; 188 | } else { 189 | $overrideRet = false; 190 | } 191 | break; 192 | //show always 193 | case 'LEVEL2': 194 | case 'LEVEL2a': 195 | $overrideRet = false; 196 | break; 197 | //(unexpected; hide always) 198 | default: 199 | $overrideRet = true; 200 | break; 201 | } 202 | 203 | if ($originalRet !== $overrideRet) { 204 | //error_log($tag . "override specific to ".$overrideRet); 205 | } 206 | 207 | return $overrideRet; 208 | } 209 | 210 | //check for generic override 211 | $tag2parts = explode(':',$tag); 212 | $tag2parts[1] = '*'; 213 | $tag2 = implode(':',$tag2parts); 214 | 215 | $tag2 = $this->compressTag($tag2); 216 | $pref = $this->vesta_symbol . $this->house_symbol . $tag2; 217 | 218 | //error_log("override? " . $pref); 219 | 220 | $override = $tree->getPreference($pref); 221 | if ($override !== '') { 222 | switch ($override) { 223 | //hide always 224 | case 'LEVEL0': 225 | $overrideRet = true; 226 | break; 227 | //hide for new individual, show otherwise 228 | case 'LEVEL1': 229 | case 'LEVEL1a': 230 | if ($this->forNewIndividual) { 231 | $overrideRet = true; 232 | } else { 233 | $overrideRet = false; 234 | } 235 | break; 236 | //show always 237 | case 'LEVEL2': 238 | case 'LEVEL2a': 239 | $overrideRet = false; 240 | break; 241 | //(unexpected; hide always) 242 | default: 243 | $overrideRet = true; 244 | break; 245 | } 246 | 247 | if ($originalRet !== $overrideRet) { 248 | //error_log($tag2 . " override generic to ".$overrideRet); 249 | } 250 | 251 | return $overrideRet; 252 | } 253 | 254 | return $originalRet; 255 | } 256 | 257 | public function alwaysExpandSubtags( 258 | Tree $tree, 259 | string $tag): bool { 260 | 261 | //check for specific override 262 | $tag = $this->compressTag($tag); 263 | $pref = $this->vesta_symbol . $this->house_symbol . $tag; 264 | 265 | //error_log("override? " . $pref); 266 | 267 | $override = $tree->getPreference($pref); 268 | if ($override !== '') { 269 | switch ($override) { 270 | case 'LEVEL1a': 271 | case 'LEVEL2a': 272 | return true; 273 | default: 274 | break; 275 | } 276 | } 277 | 278 | //check for generic override 279 | $tag2parts = explode(':',$tag); 280 | $tag2parts[1] = '*'; 281 | $tag2 = implode(':',$tag2parts); 282 | 283 | $tag2 = $this->compressTag($tag2); 284 | $pref = $this->vesta_symbol . $this->house_symbol . $tag2; 285 | 286 | //error_log("override? " . $pref); 287 | 288 | $override = $tree->getPreference($pref); 289 | if ($override !== '') { 290 | switch ($override) { 291 | case 'LEVEL1a': 292 | case 'LEVEL2a': 293 | return true; 294 | default: 295 | break; 296 | } 297 | } 298 | 299 | return false; 300 | } 301 | 302 | protected function compressTag(string $tag): string { 303 | //we only have 32 chars in table column! 304 | //(first 2 of those are used for pref outside tag, i.e. 30 left 305 | //note that this may still be problematic for custom tags) 306 | $tagCompressed = $tag; 307 | $tagCompressed = str_replace("INDI","I", $tagCompressed); 308 | $tagCompressed = str_replace("FAM","F", $tagCompressed); 309 | $tagCompressed = str_replace("ASSO","A", $tagCompressed); 310 | $tagCompressed = str_replace("SOUR","S", $tagCompressed); 311 | $tagCompressed = str_replace("PLAC","P", $tagCompressed); 312 | 313 | return $tagCompressed; 314 | } 315 | } 316 | --------------------------------------------------------------------------------