diff --git a/app/Console/Commands/CheckPasswordExpiry.php b/app/Console/Commands/CheckPasswordExpiry.php
new file mode 100644
index 0000000..01634bc
--- /dev/null
+++ b/app/Console/Commands/CheckPasswordExpiry.php
@@ -0,0 +1,81 @@
+argument('ope_id');
+
+ if ($opeId) {
+ $ope = Ope::find($opeId);
+ if (!$ope) {
+ $this->error("Ope with ID {$opeId} not found");
+ return 1;
+ }
+ $this->checkOpe($ope);
+ } else {
+ // すべてのオペレータをチェック
+ $opes = Ope::all();
+ foreach ($opes as $ope) {
+ $this->checkOpe($ope);
+ }
+ }
+
+ return 0;
+ }
+
+ /**
+ * 単一オペレータの有効期限をチェック
+ */
+ private function checkOpe(Ope $ope): void
+ {
+ $this->info("=== Ope ID: {$ope->ope_id} ({$ope->ope_name}) ===");
+ $this->info("ope_pass_changed_at: " . ($ope->ope_pass_changed_at ?? 'NULL'));
+
+ if (is_null($ope->ope_pass_changed_at)) {
+ $this->warn("❌ Password change REQUIRED (never changed)");
+ return;
+ }
+
+ try {
+ $changedAt = Carbon::parse($ope->ope_pass_changed_at);
+ $now = Carbon::now();
+ $monthsDiff = $now->diffInMonths($changedAt);
+
+ $this->info("Changed At: " . $changedAt->format('Y-m-d H:i:s'));
+ $this->info("Now: " . $now->format('Y-m-d H:i:s'));
+ $this->info("Months Difference: {$monthsDiff}");
+
+ if ($monthsDiff >= 3) {
+ $this->warn("❌ Password change REQUIRED ({$monthsDiff} months passed)");
+ } else {
+ $this->line("✅ Password is valid ({$monthsDiff} months passed, {3 - $monthsDiff} months remaining)");
+ }
+ } catch (\Exception $e) {
+ $this->error("Error parsing date: {$e->getMessage()}");
+ }
+
+ $this->line("");
+ }
+}
diff --git a/app/Http/Controllers/Admin/CityController.php b/app/Http/Controllers/Admin/CityController.php
index 82c82a7..55cba28 100644
--- a/app/Http/Controllers/Admin/CityController.php
+++ b/app/Http/Controllers/Admin/CityController.php
@@ -20,23 +20,20 @@ final class CityController extends Controller
$sortType = (string) $request->input('sort_type', 'asc');
$page = (int) $request->get('page', 1);
- // ソート許可(安全 + 規約)
- $sortable = ['city_id', 'city_name', 'print_layout', 'city_remarks', 'created_at', 'updated_at'];
- if (!in_array($sort, $sortable, true)) {
- $sort = 'city_id';
+ $query = City::query();
+
+ if ($request->filled('city_name')) {
+ $query->where('city_name', 'like', '%' . $request->input('city_name') . '%');
}
- $sortType = strtolower($sortType);
- if (!in_array($sortType, ['asc', 'desc'], true)) {
- $sortType = 'asc';
+ // 排序处理
+ if (!empty($sort)) {
+ $query->orderBy($sort, $sortType);
}
- $list = $service->paginateList(
- $request->input('city_name'),
- $sort,
- $sortType
- );
+ $list = $query->paginate(20);
+ // 页码越界处理
if ($list->total() > 0 && $page > $list->lastPage()) {
return redirect()->route('cities.index', [
'sort' => $sort,
@@ -54,56 +51,127 @@ final class CityController extends Controller
public function create(): View
{
- return view('admin.cities.create', [
- 'record' => new City(),
- ]);
- }
+ $inputs = [
+ 'city_name' => '',
+ 'print_layout' => '',
+ 'city_user' => '',
+ 'city_remarks' => '',
+ ];
- public function store(CityRequest $request, CityService $service): RedirectResponse
- {
- $service->create($request->validated());
+ if ($request->isMethod('POST')) {
+ $rules = [
+ 'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'city_remarks' => ['nullable', 'string', 'max:20'],
+ ];
+ $messages = [
+ 'city_name.required' => '市区名は必須です。',
+ 'city_name.regex' => '市区名は全角で入力してください。',
+ 'print_layout.required' => '印字レイアウトファイルは必須です。',
+ 'print_layout.regex' => '印字レイアウトファイルは全角で入力してください。',
+ 'city_user.required' => '顧客M入力不要フィールドIDは必須です。',
+ 'city_user.regex' => '顧客M入力不要フィールドIDは全角で入力してください。',
+ 'city_remarks.max' => '備考は20文字以内で入力してください。',
+ ];
+ $validator = Validator::make($request->all(), $rules, $messages);
- return redirect()
- ->route('cities.index')
- ->with('success', __('登録に成功しました'));
- }
+ $inputs = array_merge($inputs, $request->all());
- public function edit(int $id): View
- {
- $city = City::findOrFail($id);
+ if (!$validator->fails()) {
+ $maxId = DB::table('city')->max('city_id');
+ $newCityId = $maxId ? $maxId + 1 : 1;
- return view('admin.cities.edit', [
- 'record' => $city,
- ]);
- }
+ $city = new City();
+ $city->city_id = $newCityId;
+ $city->fill($request->only([
+ 'city_name',
+ 'print_layout',
+ 'city_user',
+ 'city_remarks',
+ ]));
- public function update(CityRequest $request, int $id, CityService $service): RedirectResponse
- {
- $city = City::findOrFail($id);
-
- $service->update($city, $request->validated());
-
- return redirect()
- ->route('cities.index')
- ->with('success', __('更新に成功しました'));
- }
-
- public function destroy(Request $request): RedirectResponse
- {
- $ids = $request->input('pk');
-
- // pk が単体でも配列でも受けられるようにする(編集画面/一覧画面両対応)
- if ($ids === null || $ids === '' || $ids === []) {
- return redirect()
- ->route('cities.index')
- ->with('error', __('削除する市区を選択してください。'));
+ if ($city->save()) {
+ $request->session()->flash('success', __('登録に成功しました'));
+ return redirect()->route('city');
+ } else {
+ $request->session()->flash('error', __('登録に失敗しました'));
+ }
+ } else {
+ $inputs['errorMsg'] = $validator->errors()->all();
+ }
}
- $ids = is_array($ids) ? $ids : [$ids];
- $deleted = City::destroy($ids);
-
- return $deleted
- ? redirect()->route('cities.index')->with('success', __('削除が完了しました。'))
- : redirect()->route('cities.index')->with('error', __('削除に失敗しました。'));
+ return view('admin.CityMaster.add', $inputs);
}
-}
+
+ public function edit(Request $request, $pk, $view = '')
+ {
+ $city = City::find($pk);
+ if (!$city) {
+ abort(404);
+ }
+
+ if ($request->isMethod('POST')) {
+ $rules = [
+ 'city_name' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'print_layout' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'city_user' => ['required', 'string', 'max:10', 'regex:/^[^ -~。-゚]+$/u'],
+ 'city_remarks' => ['nullable', 'string', 'max:20'],
+ ];
+ $messages = [
+ 'city_name.required' => '市区名は必須です。',
+ 'city_name.regex' => '市区名は全角で入力してください。',
+ 'print_layout.required' => '印字レイアウトファイルは必須です。',
+ 'print_layout.regex' => '印字レイアウトファイルは全角で入力してください。',
+ 'city_user.required' => '顧客M入力不要フィールドIDは必須です。',
+ 'city_user.regex' => '顧客M入力不要フィールドIDは全角で入力してください。',
+ 'city_remarks.max' => '備考は20文字以内で入力してください。',
+ ];
+ $validator = Validator::make($request->all(), $rules, $messages);
+
+ if (!$validator->fails()) {
+ $city->fill($request->only([
+ 'city_name',
+ 'print_layout',
+ 'city_user',
+ 'city_remarks',
+ ]));
+
+ if ($city->save()) {
+ $request->session()->flash('success', __('更新に成功しました'));
+ return redirect()->route('city');
+ } else {
+ $request->session()->flash('error', __('更新に失敗しました'));
+ }
+ } else {
+ return view('admin.CityMaster.edit', [
+ 'city' => $city,
+ 'errorMsg' => $validator->errors()->all(),
+ ]);
+ }
+ }
+
+ return view($view ?: 'admin.CityMaster.edit', [
+ 'city' => $city,
+ ]);
+ }
+
+ public function info(Request $request, $pk)
+ {
+ return $this->edit($request, $pk, 'CityMaster.info');
+ }
+
+ public function delete(Request $request)
+ {
+ $arr_pk = $request->get('pk');
+ if (!$arr_pk) {
+ return redirect()->route('city')->with('error', __('削除する市区を選択してください。'));
+ }
+ if (City::destroy($arr_pk)) {
+ return redirect()->route('city')->with('success', __("削除が完了しました。"));
+ } else {
+ return redirect()->route('city')->with('error', __('削除に失敗しました。'));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Http/Controllers/Admin/InformationController.php b/app/Http/Controllers/Admin/InformationController.php
index 0b87019..a44c017 100644
--- a/app/Http/Controllers/Admin/InformationController.php
+++ b/app/Http/Controllers/Admin/InformationController.php
@@ -56,6 +56,189 @@ class InformationController extends Controller
return view('admin.information.list', compact('jobs','period','type','status'));
}
+ // ダッシュボード表示
+ public function dashboard(Request $request)
+ {
+ // ダッシュボード統計情報を集計
+
+ // park_number テーブルから総容量を計算
+ // park_standard(標準) + park_number(割当)+ park_limit(制限値)の合算
+ $totalCapacity = DB::table('park_number')
+ ->selectRaw('
+ COALESCE(SUM(park_standard), 0) as std_sum,
+ COALESCE(SUM(park_number), 0) as num_sum,
+ COALESCE(SUM(park_limit), 0) as limit_sum
+ ')
+ ->first();
+
+ $totalCapacityValue = ($totalCapacity->std_sum ?? 0) +
+ ($totalCapacity->num_sum ?? 0) +
+ ($totalCapacity->limit_sum ?? 0);
+
+ // 予約待ち人数(reserve テーブルから集計)
+ // 条件:有効(valid_flag=1) かつ契約化されていない(contract_id IS NULL)
+ // キャンセル除外:reserve_cancel_flag が NULL または 0、かつ reserve_cancelday が NULL
+ $reserveQuery = DB::table('reserve')
+ ->where('valid_flag', 1)
+ ->whereNull('contract_id');
+
+ // キャンセルフラグの有無をチェック(列が存在するかどうか)
+ try {
+ $testResult = DB::table('reserve')
+ ->select(DB::raw('1'))
+ ->whereNotNull('reserve_cancel_flag')
+ ->limit(1)
+ ->first();
+
+ // 列が存在する場合、キャンセル除外条件を追加
+ $reserveQuery = $reserveQuery
+ ->where(function ($q) {
+ $q->whereNull('reserve_cancel_flag')
+ ->orWhere('reserve_cancel_flag', 0);
+ })
+ ->whereNull('reserve_cancelday');
+ } catch (\Exception $e) {
+ // キャンセルフラグが未運用の場合は基本条件のみで計算
+ }
+
+ $totalWaiting = $reserveQuery->count();
+
+ // 使用中台数(park_number の park_number が使用台数)
+ $totalUsed = DB::table('park_number')
+ ->sum('park_number') ?? 0;
+
+ // 空き台数 = 総容量 - 使用中台数
+ $totalVacant = max(0, $totalCapacityValue - $totalUsed);
+
+ // 利用率計算(小数点以下切捨て)
+ $utilizationRate = $totalCapacityValue > 0
+ ? (int) floor(($totalUsed / $totalCapacityValue) * 100)
+ : 0;
+
+ // 予約待ち率(超過時のみ、超過なしは0%)
+ // 超過判定:待機人数 > 空き台数
+ $totalWaitingRate = 0;
+ if ($totalCapacityValue > 0 && $totalWaiting > 0 && $totalWaiting > $totalVacant) {
+ // 超過分 / 総容量 * 100(分母チェック付き)
+ $totalWaitingRate = (int) floor((($totalWaiting - $totalVacant) / $totalCapacityValue) * 100);
+ }
+
+ $totalStats = [
+ 'total_cities' => DB::table('city')->count(),
+ 'total_parks' => DB::table('park')->count(),
+ 'total_contracts' => DB::table('regular_contract')->count(),
+ 'total_users' => DB::table('user')->count(),
+ 'total_devices' => DB::table('device')->count(),
+ 'today_queues' => DB::table('operator_que')
+ ->whereDate('created_at', today())
+ ->count(),
+ 'total_waiting' => $totalWaiting,
+ 'total_capacity' => $totalCapacityValue,
+ 'total_utilization_rate' => $utilizationRate,
+ 'total_vacant_number' => $totalVacant,
+ 'total_waiting_rate' => $totalWaitingRate,
+ ];
+
+ // 自治体別統計情報を作成
+ $cityStats = [];
+ $cities = DB::table('city')->get();
+
+ foreach ($cities as $city) {
+ // その自治体に属する駐輪場 ID を取得
+ $parkIds = DB::table('park')
+ ->where('city_id', $city->city_id)
+ ->pluck('park_id')
+ ->toArray();
+
+ // ① 駐輪場数
+ $parksCount = count($parkIds);
+
+ // ② 総収容台数(park_number テーブルの park_standard を合算)
+ $capacity = 0;
+ if (!empty($parkIds)) {
+ $capacityResult = DB::table('park_number')
+ ->whereIn('park_id', $parkIds)
+ ->sum('park_standard');
+ $capacity = $capacityResult ?? 0;
+ }
+
+ // ③ 契約台数(contract_cancel_flag = 0 かつ有効期間内)
+ $contractsCount = 0;
+ if (!empty($parkIds)) {
+ $contractsCount = DB::table('regular_contract')
+ ->whereIn('park_id', $parkIds)
+ ->where('contract_cancel_flag', 0)
+ ->where(function ($q) {
+ // 有効期間内:開始日 <= 今日 かつ 終了日 >= 今日
+ $q->where('contract_periods', '<=', now())
+ ->where('contract_periode', '>=', now());
+ })
+ ->count();
+ }
+
+ // ④ 利用率計算(小数点以下切捨て)
+ $utilizationRate = $capacity > 0
+ ? (int) floor(($contractsCount / $capacity) * 100)
+ : 0;
+
+ // ⑤ 空き台数
+ $availableSpaces = max(0, $capacity - $contractsCount);
+
+ // ⑥ 予約待ち人数(reserve テーブルで contract_id IS NULL かつ valid_flag = 1)
+ $waitingCount = 0;
+ if (!empty($parkIds)) {
+ $waitingQuery = DB::table('reserve')
+ ->whereIn('park_id', $parkIds)
+ ->where('valid_flag', 1)
+ ->whereNull('contract_id');
+
+ // キャンセルフラグの有無をチェック
+ try {
+ DB::table('reserve')
+ ->select(DB::raw('1'))
+ ->whereNotNull('reserve_cancel_flag')
+ ->limit(1)
+ ->first();
+
+ // 列が存在する場合、キャンセル除外条件を追加
+ $waitingQuery = $waitingQuery
+ ->where(function ($q) {
+ $q->whereNull('reserve_cancel_flag')
+ ->orWhere('reserve_cancel_flag', 0);
+ })
+ ->whereNull('reserve_cancelday');
+ } catch (\Exception $e) {
+ // キャンセルフラグが未運用の場合は基本条件のみで計算
+ }
+
+ $waitingCount = $waitingQuery->count();
+ }
+
+ // ⑦ 利用者数(ユニークユーザー数)
+ $usersCount = 0;
+ if (!empty($parkIds)) {
+ $usersCount = DB::table('regular_contract')
+ ->whereIn('park_id', $parkIds)
+ ->distinct()
+ ->count('user_id');
+ }
+
+ // 配列に追加
+ $cityStats[] = [
+ 'city' => $city,
+ 'parks_count' => $parksCount,
+ 'contracts_count' => $contractsCount,
+ 'users_count' => $usersCount,
+ 'waiting_count' => $waitingCount,
+ 'capacity' => $capacity,
+ 'utilization_rate' => $utilizationRate,
+ 'available_spaces' => $availableSpaces,
+ ];
+ }
+
+ return view('admin.information.dashboard', compact('totalStats', 'cityStats'));
+ }
+
// ステータス一括更新(着手=2 / 対応完了=3)
public function updateStatus(Request $request)
{
diff --git a/app/Http/Controllers/Admin/OpeController.php b/app/Http/Controllers/Admin/OpeController.php
index d1e6c88..183b888 100644
--- a/app/Http/Controllers/Admin/OpeController.php
+++ b/app/Http/Controllers/Admin/OpeController.php
@@ -1,4 +1,5 @@
isMethod('get')) {
-
- return view('admin.opes.add', [
+ // ※機能(画面)一覧を取得(プルダウン用)
+ $features = Feature::query()
+ ->orderBy('id')
+ ->get(['id', 'name']);
+ // ※操作権限一覧を取得(チェックボックス用)
+ $permissions = Permission::query()
+ ->orderBy('id')
+ ->get(['id', 'code', 'name']);
+
+ if ($request->isMethod('get')) {
+ return view('admin.opes.add', [
'isEdit' => false,
- 'record' => new Ope(),
- 'ope_id' => null,
- 'ope_name' => '',
- 'ope_type' => '',
- 'ope_mail' => '',
- 'ope_phone'=> '',
+ 'record' => new Ope(),
+
+ 'ope_id' => null,
+ 'ope_name' => '',
+ 'ope_type' => '',
+ 'ope_mail' => '',
+ 'ope_phone' => '',
'ope_sendalart_que1' => 0, 'ope_sendalart_que2' => 0, 'ope_sendalart_que3' => 0,
'ope_sendalart_que4' => 0, 'ope_sendalart_que5' => 0, 'ope_sendalart_que6' => 0,
'ope_sendalart_que7' => 0, 'ope_sendalart_que8' => 0, 'ope_sendalart_que9' => 0,
- 'ope_sendalart_que10'=> 0, 'ope_sendalart_que11'=> 0, 'ope_sendalart_que12'=> 0,
- 'ope_sendalart_que13'=> 0,
+ 'ope_sendalart_que10' => 0, 'ope_sendalart_que11' => 0, 'ope_sendalart_que12' => 0,
+ 'ope_sendalart_que13' => 0,
'ope_auth1' => '', 'ope_auth2' => '', 'ope_auth3' => '', 'ope_auth4' => '',
'ope_quit_flag' => 0, 'ope_quitday' => '',
+
+ // ▼追加:権限設定UI用
+ 'features' => $features,
+ 'permissions' => $permissions,
+ 'selectedFeatureId' => old('feature_id', null),
]);
}
@@ -96,36 +113,73 @@ class OpeController extends Controller
return redirect()->route('opes')->with('success', '登録しました。');
}
-
-
/**
* 編集(GET 画面 / POST 更新)
+ * ※権限(自治体×機能×操作)も同画面で設定する
*/
public function edit($id, Request $request)
{
$ope = Ope::getByPk($id);
if (!$ope) abort(404);
+ // ※機能(画面)一覧を取得(プルダウン用)
+ $features = Feature::query()
+ ->orderBy('id')
+ ->get(['id', 'name']);
+
+ // ※操作権限一覧を取得(チェックボックス用)
+ $permissions = Permission::query()
+ ->orderBy('id')
+ ->get(['id', 'code', 'name']);
+
+ // ※自治体ID(opeに紐づく想定)
+ $municipalityId = (int)($ope->municipality_id ?? 0);
+
if ($request->isMethod('get')) {
return view('admin.opes.edit', [
'isEdit' => true,
'record' => $ope,
+
+ // ▼追加:権限設定UI用
+ 'features' => $features,
+ 'permissions' => $permissions,
+ 'selectedFeatureId' => old('feature_id', null),
]);
}
+ /**
+ * ▼権限設定の保存(feature_id + permission_ids[])
+ * ※画面側の保存ボタンを「権限も同時保存」にする場合はここで処理する
+ * ※もし「基本情報の更新」と「権限更新」をボタンで分けたい場合は、別アクションに分離推奨
+ */
+ if ($request->has('feature_id')) {
+ $request->validate([
+ 'feature_id' => ['required', 'integer', 'exists:features,id'],
+ 'permission_ids' => ['nullable', 'array'],
+ 'permission_ids.*' => ['integer', 'exists:permissions,id'],
+ ]);
+
+ $featureId = (int)$request->input('feature_id');
+ $permissionIds = array_map('intval', (array)$request->input('permission_ids', []));
+
+ DB::transaction(function () use ($municipalityId, $featureId, $permissionIds) {
+ // ※機能単位で置換(自治体単位)
+ OpePermission::replaceByFeature($municipalityId, $featureId, $permissionIds);
+ });
+ }
+
// 入力値を一旦取得
$data = $request->all();
// --- バリデーション ---
$rules = [
- 'login_id' => "required|string|max:255|unique:ope,login_id,{$id},ope_id", // 編集時は自分を除外
+ 'login_id' => "required|string|max:255|unique:ope,login_id,{$id},ope_id",
'ope_name' => 'required|string|max:255',
'ope_type' => 'required|string|max:50',
'ope_phone' => 'nullable|string|max:50',
'ope_mail' => [
'required',
function ($attribute, $value, $fail) {
- // , でも ; でもOKにする
$emails = array_map('trim', explode(';', str_replace(',', ';', $value)));
foreach ($emails as $email) {
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
@@ -134,7 +188,7 @@ class OpeController extends Controller
}
}
],
- 'password' => 'nullable|string|min:8|confirmed', // 編集時は任意
+ 'password' => 'nullable|string|min:8|confirmed',
];
$request->validate($rules);
@@ -157,18 +211,42 @@ class OpeController extends Controller
return redirect()->route('opes')->with('success', '更新しました。');
}
+ /**
+ * 権限回顧(AJAX)
+ * /opes/{id}/permissions?feature_id=xx
+ * ※ope_permissionが自治体単位のため、opeの自治体IDで取得する
+ */
+ public function getPermissionsByFeature(int $id, Request $request)
+ {
+ $ope = Ope::getByPk($id);
+ if (!$ope) abort(404);
+
+ $featureId = (int)$request->query('feature_id');
+ if ($featureId <= 0) {
+ return response()->json([]);
+ }
+
+ $municipalityId = (int)($ope->municipality_id ?? 0);
+
+ $ids = OpePermission::query()
+ ->where('municipality_id', $municipalityId)
+ ->where('feature_id', $featureId)
+ ->pluck('permission_id')
+ ->values();
+
+ return response()->json($ids);
+ }
/**
* 削除(単体 or 複数)
*/
-
public function delete(Request $request)
{
$ids = [];
// 単体削除
if ($request->filled('id')) {
- $ids[] = (int) $request->input('id');
+ $ids[] = (int)$request->input('id');
}
// 複数削除
diff --git a/app/Http/Controllers/Admin/ParkingRegulationsController.php b/app/Http/Controllers/Admin/ParkingRegulationsController.php
new file mode 100644
index 0000000..af26ce6
--- /dev/null
+++ b/app/Http/Controllers/Admin/ParkingRegulationsController.php
@@ -0,0 +1,204 @@
+validate([
+ 'park_id' => 'required|integer|exists:park,park_id',
+ ], [
+ 'park_id.required' => '駐輪場IDは必須です。',
+ 'park_id.integer' => '駐輪場IDは整数である必要があります。',
+ 'park_id.exists' => '指定された駐輪場が見つかりません。',
+ ]);
+
+ $parkId = (int) $request->input('park_id');
+
+ // 駐輪場情報取得
+ $park = Park::where('park_id', $parkId)->firstOrFail();
+
+ // parking_regulations を取得し、psection / ptype 名を JOIN して表示
+ $data = DB::table('parking_regulations')
+ ->leftJoin('psection', 'parking_regulations.psection_id', '=', 'psection.psection_id')
+ ->leftJoin('ptype', 'parking_regulations.ptype_id', '=', 'ptype.ptype_id')
+ ->where('parking_regulations.park_id', $parkId)
+ ->select(
+ 'parking_regulations.parking_regulations_seq',
+ 'parking_regulations.park_id',
+ 'parking_regulations.psection_id',
+ 'parking_regulations.ptype_id',
+ 'parking_regulations.regulations_text',
+ 'psection.psection_subject as psection_subject',
+ 'ptype.ptype_subject as ptype_subject'
+ )
+ ->orderBy('parking_regulations.psection_id')
+ ->orderBy('parking_regulations.ptype_id')
+ ->paginate(50);
+
+ return view('admin.parking_regulations.list', [
+ 'park' => $park,
+ 'parkId' => $parkId,
+ 'regulations' => $data,
+ ]);
+ }
+
+ /**
+ * 新規作成フォーム表示/登録
+ */
+ public function add(Request $request)
+ {
+ $parkId = $request->input('park_id');
+
+ // 駐輪場存在確認
+ if (!$parkId) {
+ return redirect()->back()->withErrors(['park_id' => '駐輪場IDが指定されていません。']);
+ }
+
+ $park = Park::where('park_id', $parkId)->firstOrFail();
+
+ // マスタの選択肢取得
+ $psections = DB::table('psection')->orderBy('psection_id')->get();
+ $ptypes = DB::table('ptype')->orderBy('ptype_id')->get();
+
+ if ($request->isMethod('post')) {
+ // 登録処理
+ $validated = $request->validate([
+ 'park_id' => 'required|integer|exists:park,park_id',
+ 'psection_id' => 'required|integer',
+ 'ptype_id' => 'required|integer',
+ 'regulations_text' => 'nullable|string',
+ ], [
+ 'park_id.required' => '駐輪場IDは必須です。',
+ 'psection_id.required' => '車種区分は必須です。',
+ 'ptype_id.required' => '駐輪分類は必須です。',
+ ]);
+
+ // 重複チェック
+ $exists = DB::table('parking_regulations')
+ ->where('park_id', $validated['park_id'])
+ ->where('psection_id', $validated['psection_id'])
+ ->where('ptype_id', $validated['ptype_id'])
+ ->exists();
+
+ if ($exists) {
+ return back()->withErrors(['duplicate' => '同じ組み合わせの規定が既に存在します。'])->withInput();
+ }
+
+ ParkingRegulation::create([
+ 'park_id' => $validated['park_id'],
+ 'psection_id' => $validated['psection_id'],
+ 'ptype_id' => $validated['ptype_id'],
+ 'regulations_text' => $validated['regulations_text'] ?? null,
+ ]);
+
+ return redirect()->route('parking_regulations_list', ['park_id' => $validated['park_id']])->with('success', '登録しました。');
+ }
+
+ return view('admin.parking_regulations.add', [
+ 'park' => $park,
+ 'parkId' => $parkId,
+ 'psections' => $psections,
+ 'ptypes' => $ptypes,
+ ]);
+ }
+
+ /**
+ * 編集フォーム表示
+ */
+ public function edit($seq, Request $request)
+ {
+ $record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
+ if (!$record) {
+ return redirect()->back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
+ }
+
+ $park = Park::where('park_id', $record->park_id)->firstOrFail();
+
+ $psections = DB::table('psection')->orderBy('psection_id')->get();
+ $ptypes = DB::table('ptype')->orderBy('ptype_id')->get();
+
+ return view('admin.parking_regulations.edit', [
+ 'park' => $park,
+ 'record' => $record,
+ 'psections' => $psections,
+ 'ptypes' => $ptypes,
+ ]);
+ }
+
+ /**
+ * 更新処理
+ */
+ public function update($seq, Request $request)
+ {
+ $validated = $request->validate([
+ 'psection_id' => 'required|integer',
+ 'ptype_id' => 'required|integer',
+ 'regulations_text' => 'nullable|string',
+ ], [
+ 'psection_id.required' => '車種区分は必須です。',
+ 'ptype_id.required' => '駐輪分類は必須です。',
+ ]);
+
+ // 対象レコード取得
+ $record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
+ if (!$record) {
+ return back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
+ }
+
+ // 重複チェック(自分自身は除外)
+ $exists = DB::table('parking_regulations')
+ ->where('park_id', $record->park_id)
+ ->where('psection_id', $validated['psection_id'])
+ ->where('ptype_id', $validated['ptype_id'])
+ ->where('parking_regulations_seq', '<>', $seq)
+ ->exists();
+
+ if ($exists) {
+ return back()->withErrors(['duplicate' => '同じ組み合わせの規定が既に存在します。'])->withInput();
+ }
+
+ DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->update([
+ 'psection_id' => $validated['psection_id'],
+ 'ptype_id' => $validated['ptype_id'],
+ 'regulations_text' => $validated['regulations_text'] ?? null,
+ 'updated_at' => now(),
+ ]);
+
+ return redirect()->route('parking_regulations_list', ['park_id' => $record->park_id])->with('success', '更新しました。');
+ }
+
+ /**
+ * 削除処理
+ */
+ public function delete(Request $request)
+ {
+ $validated = $request->validate([
+ 'parking_regulations_seq' => 'required|integer',
+ ], [
+ 'parking_regulations_seq.required' => '削除対象が指定されていません。',
+ ]);
+
+ $seq = (int) $validated['parking_regulations_seq'];
+
+ $record = DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->first();
+ if (!$record) {
+ return back()->withErrors(['not_found' => '指定の規定が見つかりません。']);
+ }
+
+ DB::table('parking_regulations')->where('parking_regulations_seq', $seq)->delete();
+
+ return redirect()->route('parking_regulations_list', ['park_id' => $record->park_id])->with('success', '削除しました。');
+ }
+}
diff --git a/app/Http/Controllers/Admin/ReductionConfirmMasterController.php b/app/Http/Controllers/Admin/ReductionConfirmMasterController.php
new file mode 100644
index 0000000..6c3923a
--- /dev/null
+++ b/app/Http/Controllers/Admin/ReductionConfirmMasterController.php
@@ -0,0 +1,120 @@
+validate([
+ 'park_id' => 'required|integer|exists:park,park_id',
+ ], [
+ 'park_id.required' => '駐輪場IDは必須です。',
+ 'park_id.integer' => '駐輪場IDは整数である必要があります。',
+ 'park_id.exists' => '指定された駐輪場が見つかりません。',
+ ]);
+
+ $parkId = (int) $request->input('park_id');
+
+ // 駐輪場情報を取得
+ $park = Park::where('park_id', $parkId)->firstOrFail();
+
+ // reduction_confirm を主テーブルとして、usertype と JOIN して一覧を取得
+ // WHERE park_id = ? で対象駐輪場のレコードのみ取得
+ $reductionData = DB::table('reduction_confirm')
+ ->leftJoin('usertype', 'reduction_confirm.user_categoryid', '=', 'usertype.user_categoryid')
+ ->where('reduction_confirm.park_id', $parkId)
+ ->orderBy('reduction_confirm.user_categoryid', 'asc')
+ ->select(
+ 'reduction_confirm.park_id',
+ 'reduction_confirm.user_categoryid',
+ 'reduction_confirm.reduction_confirm_type',
+ 'usertype.usertype_subject1',
+ 'usertype.usertype_subject2',
+ 'usertype.usertype_subject3'
+ )
+ ->paginate(50);
+
+ return view('admin.reduction_confirm.list', [
+ 'park' => $park,
+ 'parkId' => $parkId,
+ 'reductionData' => $reductionData,
+ ]);
+ }
+
+ /**
+ * 減免確認情報を一括更新
+ *
+ * @param Request $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function store(Request $request)
+ {
+ // バリデーション
+ $validated = $request->validate([
+ 'park_id' => 'required|integer|exists:park,park_id',
+ 'row_user_categoryid' => 'array',
+ 'row_user_categoryid.*' => 'integer',
+ 'reduction_confirm_type' => 'array',
+ 'reduction_confirm_type.*' => 'in:0,1,2',
+ ], [
+ 'park_id.required' => '駐輪場IDは必須です。',
+ 'park_id.integer' => '駐輪場IDは整数である必要があります。',
+ 'park_id.exists' => '指定された駐輪場が見つかりません。',
+ 'reduction_confirm_type.*.in' => '減免確認種別は0, 1, 2 のいずれかである必要があります。',
+ ]);
+
+ $parkId = (int) $validated['park_id'];
+
+ // ログイン中のオペレータID取得
+ $opeId = auth()->user()->ope_id ?? null;
+
+ // POST された配列は index ベースで来るため、row_user_categoryid のインデックスに合わせてマッピングする
+ $rowUserCategory = $request->input('row_user_categoryid', []);
+ $types = $request->input('reduction_confirm_type', []);
+
+ try {
+ DB::transaction(function () use ($parkId, $rowUserCategory, $types, $opeId) {
+ foreach ($rowUserCategory as $idx => $userCategoryId) {
+ if (!isset($types[$idx])) {
+ continue;
+ }
+ $type = (int) $types[$idx];
+
+ DB::table('reduction_confirm')
+ ->where('park_id', $parkId)
+ ->where('user_categoryid', (int) $userCategoryId)
+ ->update([
+ 'reduction_confirm_type' => $type,
+ 'updated_at' => now(),
+ 'ope_id' => $opeId,
+ ]);
+ }
+ });
+
+ return redirect()->route('reduction_confirm_list', ['park_id' => $parkId])
+ ->with('success', '減免確認マスタを更新しました。');
+ } catch (\Exception $e) {
+ \Log::error('ReductionConfirm update failed', [
+ 'park_id' => $parkId,
+ 'error' => $e->getMessage(),
+ ]);
+
+ return back()->withErrors(['error' => '更新に失敗しました。管理者にお問い合わせください。'])
+ ->withInput();
+ }
+ }
+}
+
diff --git a/app/Http/Controllers/Auth/EmailOtpController.php b/app/Http/Controllers/Auth/EmailOtpController.php
new file mode 100644
index 0000000..fad9c6f
--- /dev/null
+++ b/app/Http/Controllers/Auth/EmailOtpController.php
@@ -0,0 +1,138 @@
+otpService = $otpService;
+ }
+
+ /**
+ * OTP 入力フォームを表示
+ *
+ * ログイン直後、ユーザーに6桁の OTP コードを入力させるページを表示します
+ * メールアドレスはマスク表示(例:a***@example.com)
+ */
+ public function show(Request $request)
+ {
+ /** @var Ope */
+ $user = $request->user();
+
+ // メールアドレスをマスク(最初の1文字のみ表示)
+ $maskedEmail = $this->otpService->maskEmail($user->ope_mail);
+
+ // 次の重発までの待機時間
+ $resendWaitSeconds = $this->otpService->getResendWaitSeconds($user);
+
+ return view('auth.otp', [
+ 'maskedEmail' => $maskedEmail,
+ 'resendWaitSeconds' => $resendWaitSeconds,
+ ]);
+ }
+
+ /**
+ * OTP コード検証
+ *
+ * ユーザーが入力した6桁のコードを検証します
+ *
+ * 成功時:email_otp_verified_at を更新し、ホームページにリダイレクト
+ * 失敗時:エラーメッセージと共に OTP 入力フォームに戻す
+ */
+ public function verify(Request $request)
+ {
+ // 入力値を検証
+ $validated = $this->validate($request, [
+ 'code' => ['required', 'string', 'size:6', 'regex:/^\d{6}$/'],
+ ], [
+ 'code.required' => 'OTPコードは必須です。',
+ 'code.size' => 'OTPコードは6桁である必要があります。',
+ 'code.regex' => 'OTPコードは6桁の数字である必要があります。',
+ ]);
+
+ /** @var Ope */
+ $user = $request->user();
+
+ // OTP コードを検証
+ if ($this->otpService->verify($user, $validated['code'])) {
+ // 検証成功:ホームページにリダイレクト
+ return redirect()->intended(route('home'))
+ ->with('success', 'OTP認証が完了しました。');
+ }
+
+ // 検証失敗:エラーメッセージと共に戻す
+ return back()
+ ->withInput()
+ ->with('error', '無効なまたは有効期限切れのOTPコードです。');
+ }
+
+ /**
+ * OTP コード再送
+ *
+ * ユーザーが OTP コード再送をリクエストした場合に実行
+ * 60秒以内の連続再送はブロックします
+ */
+ public function resend(Request $request)
+ {
+ /** @var Ope */
+ $user = $request->user();
+
+ // 重発可能か確認
+ if (!$this->otpService->canResend($user)) {
+ $waitSeconds = $this->otpService->getResendWaitSeconds($user);
+
+ return back()->with('error', "後 {$waitSeconds} 秒待機してからリクエストしてください。");
+ }
+
+ try {
+ // 新しい OTP コードを発行
+ $otpCode = $this->otpService->issue($user);
+
+ // ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出
+ $operatorEmails = explode(';', trim($user->ope_mail));
+ $primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail);
+
+ Log::info('OTP 再送メール送信開始: ' . $primaryEmail);
+
+ // メール送信
+ Mail::to($primaryEmail)->send(new EmailOtpMail(
+ $otpCode,
+ $user->name ?? 'ユーザー'
+ ));
+
+ Log::info('OTP 再送メール送信完了: ' . $primaryEmail);
+
+ return back()->with('success', 'OTPコードを再送信しました。');
+ } catch (\Exception $e) {
+ Log::error('OTP resend error: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'user_id' => $user->ope_id ?? null,
+ 'user_email' => $user->ope_mail ?? null,
+ ]);
+
+ return back()->with('error', 'OTP送信に失敗しました。もう一度お試しください。');
+ }
+ }
+}
diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php
index c764940..2b34eb9 100644
--- a/app/Http/Controllers/Auth/ForgotPasswordController.php
+++ b/app/Http/Controllers/Auth/ForgotPasswordController.php
@@ -7,6 +7,8 @@ use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
+use Illuminate\Support\Facades\Log;
+use Carbon\Carbon;
class ForgotPasswordController extends Controller
{
@@ -37,25 +39,74 @@ class ForgotPasswordController extends Controller
return back()->withErrors(['email' => '該当するユーザーが見つかりません。']);
}
+ // 5分間隔のメール送信制限チェック(最新のトークンを対象)
+ $lastToken = DB::table('password_reset_tokens')
+ ->where('ope_mail', $user->ope_mail)
+ ->orderByDesc('created_at')
+ ->first();
+ if ($lastToken) {
+ // タイムゾーンを明示的に指定(デフォルトはUTCで解析される可能性がある)
+ $lastCreatedAt = Carbon::parse($lastToken->created_at, config('app.timezone'));
+ $now = now();
+
+ // 経過秒数で判定
+ $diffSeconds = $lastCreatedAt->diffInSeconds(now(), false);
+ $limitSeconds = 5 * 60; // 5分
+ if ($diffSeconds < $limitSeconds) {
+ $remainSeconds = $limitSeconds - $diffSeconds;
+ // 残り秒を「分」に変換:端数は切り上げ(例:1秒残りでも1分と表示)
+ $waitMinutes = (int) ceil($remainSeconds / 60);
+ return back()->withErrors([
+ 'email' => "パスワード再設定メールは5分以上の間隔を置いて送信してください。{$waitMinutes}分後に再度お試しください。"
+ ]);
+ }
+ }
+
// トークン生成
$token = Str::random(60);
+ // SHA256ハッシュで保存(セキュリティ向上)
+ $tokenHash = hash('sha256', $token);
// トークン保存(既存レコードがあれば更新)
DB::table('password_reset_tokens')->updateOrInsert(
['ope_mail' => $user->ope_mail],
[
- 'token' => $token,
+ 'token' => $tokenHash,
'created_at' => now(),
]
);
// メール送信
- $resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
- Mail::raw("下記URLからパスワード再設定を行ってください。\n\n{$resetUrl}", function ($message) use ($user) {
- $message->to($user->ope_mail)
- ->subject('パスワード再設定のご案内');
- });
+ try {
+ $resetUrl = url('/reset-password?token=' . $token . '&email=' . urlencode($user->ope_mail));
+
+ $body = $user->ope_name . " 様\n\n" .
+ "So-Managerをご利用いただき、ありがとうございます。\n\n" .
+ "本メールは、パスワード再設定のご依頼を受けてお送りしております。\n\n" .
+ "以下のURLをクリックし、新しいパスワードを設定してください。\n\n" .
+ $resetUrl . "\n\n" .
+ "※このURLの有効期限は、24時間です。\n" .
+ "※有効期限を過ぎた場合は、再度パスワード再設定手続きを行ってください。\n" .
+ "※本メールにお心当たりがない場合は、本メールを破棄してください。\n\n" .
+ "_________________________________\n" .
+ "So-Manager サポートセンター\n" .
+ "E-mail : support@so-manager.com\n" .
+ "URL : https://www.so-manager.com/\n" .
+ "_________________________________";
+
+ Mail::raw($body, function ($message) use ($user) {
+ $message->to($user->ope_mail)
+ ->from(config('mail.from.address'), config('mail.from.name'))
+ ->subject('【【So-Manager】パスワード再設定のご案内】');
+ });
+ } catch (\Throwable $e) {
+ Log::error('ForgotPassword mail send failed', [
+ 'to' => $user->ope_mail,
+ 'error' => $e->getMessage(),
+ ]);
+ return back()->withErrors(['email' => 'メール送信に失敗しました。サーバログを確認してください。']);
+ }
return back()->with('status', 'パスワード再設定メールを送信しました。');
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php
index 9f8bce6..18ed0ba 100644
--- a/app/Http/Controllers/Auth/LoginController.php
+++ b/app/Http/Controllers/Auth/LoginController.php
@@ -3,8 +3,12 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
+use App\Mail\EmailOtpMail;
+use App\Services\EmailOtpService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
@@ -135,6 +139,10 @@ class LoginController extends Controller
/**
* ログイン成功時のレスポンス
*
+ * OTP認証チェック:
+ * - 24時間以内に OTP 認証済みの場合:/home にリダイレクト
+ * - 未認証の場合:OTP メール送信 → /otp にリダイレクト
+ *
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
@@ -147,7 +155,45 @@ class LoginController extends Controller
// ここで保持する値も login_id(入力名は ope_id のまま)
$request->session()->put('login_ope_id', $request->input('ope_id'));
- return redirect()->intended($this->redirectTo);
+ // OTP認証チェック
+ $otpService = app(EmailOtpService::class);
+ $user = Auth::user();
+
+ // 24時間以内に OTP 認証済みの場合
+ if ($otpService->isOtpRecent($user)) {
+ return redirect()->intended($this->redirectTo);
+ }
+
+ // OTP 未認証の場合:OTP コード発行 → メール送信 → /otp にリダイレクト
+ try {
+ $otpCode = $otpService->issue($user);
+
+ // ope_mail はセミコロン区切りで複数アドレスを保持する可能性があるため、最初のアドレスのみ抽出
+ $operatorEmails = explode(';', trim($user->ope_mail));
+ $primaryEmail = trim($operatorEmails[0] ?? $user->ope_mail);
+
+ Log::info('OTP メール送信開始: ' . $primaryEmail);
+
+ Mail::to($primaryEmail)->send(new EmailOtpMail(
+ $otpCode,
+ $user->name ?? 'ユーザー'
+ ));
+
+ Log::info('OTP メール送信完了: ' . $primaryEmail);
+
+ return redirect()->route('otp.show')
+ ->with('info', 'OTP認証コードをメール送信しました。');
+ } catch (\Exception $e) {
+ Log::error('OTP issue/send failed: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'user_id' => $user->ope_id ?? null,
+ 'user_email' => $user->ope_mail ?? null,
+ ]);
+
+ // メール送信エラー時は home にリダイレクトするか、カスタムエラーを返す
+ return redirect($this->redirectTo)
+ ->with('warning', 'OTP認証メールの送信に失敗しました。');
+ }
}
/**
diff --git a/app/Http/Controllers/Auth/PasswordChangeController.php b/app/Http/Controllers/Auth/PasswordChangeController.php
new file mode 100644
index 0000000..e8dfd9e
--- /dev/null
+++ b/app/Http/Controllers/Auth/PasswordChangeController.php
@@ -0,0 +1,135 @@
+isPasswordChangeRequired($ope);
+
+ return view('auth.password-change', [
+ 'isRequired' => $isRequired,
+ ]);
+ }
+
+ /**
+ * パスワード変更成功画面を表示
+ *
+ * GET /password/change/success
+ *
+ * @return \Illuminate\View\View
+ */
+ public function showSuccessPage()
+ {
+ return view('auth.password-change-success');
+ }
+
+ /**
+ * パスワード変更処理
+ *
+ * POST /password/change
+ *
+ * バリデーション:
+ * - 当前パスワード:必填、8-64文字、ハッシュ値一致確認
+ * - 新パスワード:必填、8-64文字、英数字+記号のみ、当前と異なる
+ * - 新パスワード確認:必填、新パスワードと一致
+ *
+ * @param \App\Http\Requests\ChangePasswordRequest $request
+ * @return \Illuminate\Http\RedirectResponse
+ */
+ public function updatePassword(ChangePasswordRequest $request)
+ {
+ // 現在のユーザーを取得
+ $ope = Auth::user();
+
+ // ステップ1:当前パスワードの認証(ハッシュ値の確認)
+ if (!Hash::check($request->current_password, $ope->ope_pass)) {
+ // バリデーションエラーとして当前パスワード が正しくないことを返す
+ throw ValidationException::withMessages([
+ 'current_password' => '当前パスワードが正しくありません。',
+ ]);
+ }
+
+ // ステップ2:新パスワードが当前パスワードと同一でないか確認
+ // FormRequest側でも not_in ルールで確認しているが、ハッシュ値での二重チェック
+ if (Hash::check($request->password, $ope->ope_pass)) {
+ throw ValidationException::withMessages([
+ 'password' => '新パスワードは当前パスワードと異なる必要があります。',
+ ]);
+ }
+
+ // ステップ3:データベース更新
+ // パスワードをハッシュ化して更新
+ $ope->ope_pass = Hash::make($request->password);
+
+ // パスワード変更時刻を現在時刻に更新
+ $ope->ope_pass_changed_at = Carbon::now();
+
+ // updated_at も自動更新される
+ $ope->save();
+
+ // イベント発火:パスワード変更イベント
+ event(new PasswordReset($ope));
+
+ // 成功画面へリダイレクト
+ return redirect()->route('password.change.success');
+ }
+
+ /**
+ * パスワード変更が必須かどうかを判定
+ *
+ * 初回ログイン時(ope_pass_changed_at が NULL)または
+ * 最後変更から3ヶ月以上経過している場合、TRUE を返す
+ *
+ * @param \App\Models\Ope $ope
+ * @return bool
+ */
+ private function isPasswordChangeRequired($ope): bool
+ {
+ // パスワード変更日時が未設定(初回ログイン等)
+ if (is_null($ope->ope_pass_changed_at)) {
+ return true;
+ }
+
+ // パスワード変更から経過日数を計算
+ $changedAt = Carbon::parse($ope->ope_pass_changed_at);
+ $now = Carbon::now();
+
+ // 3ヶ月以上経過している場合
+ if ($now->diffInMonths($changedAt) >= 3) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php
index 815e80f..7641fc2 100644
--- a/app/Http/Controllers/Auth/ResetPasswordController.php
+++ b/app/Http/Controllers/Auth/ResetPasswordController.php
@@ -14,6 +14,33 @@ class ResetPasswordController extends Controller
{
$token = $request->query('token');
$email = $request->query('email');
+
+ // トークンのハッシュ化
+ $tokenHash = hash('sha256', $token);
+
+ // トークン・メール・24時間以内の有効性をチェック
+ $record = DB::table('password_reset_tokens')
+ ->where('ope_mail', $email)
+ ->where('token', $tokenHash)
+ ->first();
+
+ if (!$record) {
+ return redirect()->route('forgot_password')
+ ->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
+ }
+
+ // 24時間チェック
+ $createdAt = \Carbon\Carbon::parse($record->created_at);
+ if ($createdAt->addHours(24)->isPast()) {
+ // 期限切れトークンを削除
+ DB::table('password_reset_tokens')
+ ->where('ope_mail', $email)
+ ->delete();
+
+ return redirect()->route('forgot_password')
+ ->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
+ }
+
return view('auth.reset-password', compact('token', 'email'));
}
@@ -25,14 +52,28 @@ class ResetPasswordController extends Controller
'password' => 'required|confirmed|min:8',
]);
- // トークンチェック
+ // トークンのハッシュ化
+ $tokenHash = hash('sha256', $request->token);
+
+ // トークン・メール・24時間以内の有効性をチェック
$record = DB::table('password_reset_tokens')
->where('ope_mail', $request->email)
- ->where('token', $request->token)
+ ->where('token', $tokenHash)
->first();
if (!$record) {
- return back()->withErrors(['email' => '無効なトークンまたはメールアドレスです。']);
+ return back()->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
+ }
+
+ // 24時間チェック
+ $createdAt = \Carbon\Carbon::parse($record->created_at);
+ if ($createdAt->addHours(24)->isPast()) {
+ // 期限切れトークンを削除
+ DB::table('password_reset_tokens')
+ ->where('ope_mail', $request->email)
+ ->delete();
+
+ return back()->withErrors(['email' => 'URLの有効期限(24時間)が切れました。再度お手続きを行ってください。']);
}
// パスワード更新
@@ -42,11 +83,14 @@ class ResetPasswordController extends Controller
}
$user->password = Hash::make($request->password);
$user->updated_at = now();
+ // パスワード再設定時もope_pass_changed_atを更新
+ $user->ope_pass_changed_at = now();
$user->save();
// トークン削除
DB::table('password_reset_tokens')->where('ope_mail', $request->email)->delete();
- return redirect()->route('login')->with('status', 'パスワードを再設定しました。');
+ // パスワード再設定成功画面へリダイレクト
+ return redirect()->route('password.change.success');
}
}
\ No newline at end of file
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 8bcdd39..04f7ffe 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -3,6 +3,8 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
+use App\Models\City;
+use App\Services\MenuAccessService;
class HomeController extends Controller
{
@@ -22,10 +24,15 @@ class HomeController extends Controller
* アプリケーションのダッシュボードを表示
* 認証後のホーム画面
*
+ * @param MenuAccessService $menuAccessService メニューアクセス制御サービス
* @return \Illuminate\Http\Response
*/
- public function index()
+ public function index(MenuAccessService $menuAccessService)
{
- return view('home');
+ // ログイン中のオペレータが表示可能な自治体一覧を取得
+ $visibleCities = $menuAccessService->visibleCities();
+ $isSorin = $menuAccessService->isSorin();
+
+ return view('home', compact('visibleCities', 'isSorin'));
}
-}
\ No newline at end of file
+}
diff --git a/app/Http/Middleware/CheckCityAccess.php b/app/Http/Middleware/CheckCityAccess.php
new file mode 100644
index 0000000..068283e
--- /dev/null
+++ b/app/Http/Middleware/CheckCityAccess.php
@@ -0,0 +1,34 @@
+route('city_id');
+ // $user = auth()->user();
+ // if (!$user->canAccessCity($city_id)) {
+ // return abort(403, '指定された自治体へのアクセス権がありません。');
+ // }
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/CheckPasswordChangeRequired.php b/app/Http/Middleware/CheckPasswordChangeRequired.php
new file mode 100644
index 0000000..1ffcdf8
--- /dev/null
+++ b/app/Http/Middleware/CheckPasswordChangeRequired.php
@@ -0,0 +1,102 @@
+routeIs('password.change.show', 'password.change.update')) {
+ return $next($request);
+ }
+
+ // 現在のユーザーを取得
+ $ope = Auth::user();
+
+ // パスワード変更が必須か判定
+ if ($this->isPasswordChangeRequired($ope)) {
+ return redirect()->route('password.change.show');
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * パスワード変更が必須かどうかを判定
+ *
+ * 初回ログイン時(ope_pass_changed_at が NULL)または
+ * 最後変更から3ヶ月以上経過している場合、TRUE を返す
+ *
+ * @param \App\Models\Ope $ope
+ * @return bool
+ */
+ private function isPasswordChangeRequired($ope): bool
+ {
+ // パスワード変更日時が未設定(初回ログイン等)
+ if (is_null($ope->ope_pass_changed_at)) {
+ \Log::info('Password change required: ope_pass_changed_at is null', [
+ 'ope_id' => $ope->ope_id,
+ ]);
+ return true;
+ }
+
+ // パスワード変更から経過日数を計算
+ // ope_pass_changed_at は複数のフォーマットに対応
+ try {
+ $changedAt = Carbon::parse($ope->ope_pass_changed_at);
+ } catch (\Exception $e) {
+ // パース失敗時は強制変更
+ \Log::warning('Failed to parse ope_pass_changed_at', [
+ 'ope_id' => $ope->ope_id,
+ 'value' => $ope->ope_pass_changed_at,
+ 'error' => $e->getMessage(),
+ ]);
+ return true;
+ }
+
+ $now = Carbon::now();
+
+ // 3ヶ月以上経過しているか判定
+ // diffInMonths は絶対値ではなく符号付きなので、abs() で絶対値を取得
+ $monthsDiff = abs($now->diffInMonths($changedAt));
+
+ \Log::info('Password change check', [
+ 'ope_id' => $ope->ope_id,
+ 'changed_at' => $changedAt->format('Y-m-d H:i:s'),
+ 'now' => $now->format('Y-m-d H:i:s'),
+ 'months_diff' => $monthsDiff,
+ 'is_required' => $monthsDiff >= 3,
+ ]);
+
+ if ($monthsDiff >= 3) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/Http/Middleware/EnsureOtpVerified.php b/app/Http/Middleware/EnsureOtpVerified.php
new file mode 100644
index 0000000..44f8b75
--- /dev/null
+++ b/app/Http/Middleware/EnsureOtpVerified.php
@@ -0,0 +1,57 @@
+otpService = $otpService;
+ }
+
+ /**
+ * リクエストを処理
+ *
+ * @param Request $request
+ * @param Closure $next
+ * @return Response
+ */
+ public function handle(Request $request, Closure $next): Response
+ {
+ // ユーザーが認証されていない場合はスキップ
+ if (!$request->user()) {
+ return $next($request);
+ }
+
+ // OTP ページ関連のリクエストはスキップ(無限ループ防止)
+ // /otp /* のパターンを許可
+ if ($request->routeIs(['otp.show', 'otp.verify', 'otp.resend'])) {
+ return $next($request);
+ }
+
+ // 24時間以内に OTP 認証が完了している場合はスキップ
+ if ($this->otpService->isOtpRecent($request->user())) {
+ return $next($request);
+ }
+
+ // OTP 認証が必要な場合は OTP ページにリダイレクト
+ return redirect()->route('otp.show')
+ ->with('info', 'セキュリティ確認のため OTP 認証が必要です。');
+ }
+}
diff --git a/app/Http/Middleware/ShareMenuAccessData.php b/app/Http/Middleware/ShareMenuAccessData.php
new file mode 100644
index 0000000..bca3c9f
--- /dev/null
+++ b/app/Http/Middleware/ShareMenuAccessData.php
@@ -0,0 +1,90 @@
+ $menuAccessService->isSorin(),
+ 'visibleCities' => $menuAccessService->visibleCities(),
+ ];
+
+ // Nav bar に表示される ハード異常・タスク件数を取得
+ if (auth()->check()) {
+ // ハード異常(que_class > 99)かつステータスが未対応(1)または進行中(2)
+ $hardwareIssues = DB::table('operator_que as oq')
+ ->leftJoin('user as u', 'oq.user_id', '=', 'u.user_id')
+ ->leftJoin('park as p', 'oq.park_id', '=', 'p.park_id')
+ ->select(
+ 'oq.que_id', 'oq.que_class', 'oq.que_comment',
+ 'oq.created_at', 'oq.updated_at', 'oq.que_status'
+ )
+ ->where('oq.que_class', '>', 99)
+ ->whereIn('oq.que_status', [1, 2])
+ ->orderBy('oq.created_at', 'DESC')
+ ->limit(5)
+ ->get();
+
+ // タスク(que_class < 99)かつステータスが未対応(1)または進行中(2)
+ $taskIssues = DB::table('operator_que as oq')
+ ->leftJoin('user as u', 'oq.user_id', '=', 'u.user_id')
+ ->leftJoin('park as p', 'oq.park_id', '=', 'p.park_id')
+ ->select(
+ 'oq.que_id', 'oq.que_class', 'oq.que_comment',
+ 'oq.created_at', 'oq.updated_at', 'oq.que_status'
+ )
+ ->where('oq.que_class', '<', 99)
+ ->whereIn('oq.que_status', [1, 2])
+ ->orderBy('oq.created_at', 'DESC')
+ ->limit(5)
+ ->get();
+
+ // ハード異常・タスク件数計算
+ $hardCount = DB::table('operator_que')
+ ->where('que_class', '>', 99)
+ ->whereIn('que_status', [1, 2])
+ ->count();
+
+ $taskCount = DB::table('operator_que')
+ ->where('que_class', '<', 99)
+ ->whereIn('que_status', [1, 2])
+ ->count();
+
+ // 最新のハード異常・タスク日時
+ $hardLatest = $hardwareIssues->first()?->created_at;
+ $taskLatest = $taskIssues->first()?->created_at;
+
+ // Nav bar 関連データをマージ
+ $viewData = array_merge($viewData, [
+ 'hardCount' => $hardCount,
+ 'hardLatest' => $hardLatest,
+ 'latestHards' => $hardwareIssues,
+ 'taskCount' => $taskCount,
+ 'taskLatest' => $taskLatest,
+ 'latestTasks' => $taskIssues,
+ ]);
+ }
+
+ // すべてのビューでこれらのデータが利用可能
+ View::share($viewData);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Requests/ChangePasswordRequest.php b/app/Http/Requests/ChangePasswordRequest.php
new file mode 100644
index 0000000..eea6761
--- /dev/null
+++ b/app/Http/Requests/ChangePasswordRequest.php
@@ -0,0 +1,97 @@
+check();
+ }
+
+ /**
+ * 入力値の検証ルール
+ *
+ * クライアント側:必填(3項目)、長度 8-64、新密码仅半角英数字+记号、新/确认一致
+ * サーバー側:当前密码认证(hash check)、新密码不能等于旧密码(CustomRulesで実装)
+ *
+ * @return array|string>
+ */
+ public function rules(): array
+ {
+ return [
+ // 当前パスワード:必填、8-64文字
+ 'current_password' => [
+ 'required',
+ 'string',
+ 'min:8',
+ 'max:64',
+ ],
+
+ // 新パスワード:必填、8-64文字、英数字+記号のみ
+ 'password' => [
+ 'required',
+ 'string',
+ 'min:8',
+ 'max:64',
+ 'regex:/^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:\'",.<>?\/\\|`~]+$/', // 半角英数字+記号のみ
+ ],
+
+ // 新パスワード確認:必填、新パスワードと一致
+ 'password_confirmation' => [
+ 'required',
+ 'string',
+ 'same:password',
+ ],
+
+ // hidden フィールド(フォーム側で出力)
+ 'updated_at' => 'nullable|date_format:Y-m-d H:i:s',
+ 'ope_pass_changed_at' => 'nullable|date_format:Y-m-d H:i:s',
+ ];
+ }
+
+ /**
+ * 属性の表示名
+ *
+ * @return array
+ */
+ public function attributes(): array
+ {
+ return [
+ 'current_password' => '当前パスワード',
+ 'password' => '新パスワード',
+ 'password_confirmation' => '新パスワード確認',
+ ];
+ }
+
+ /**
+ * 検証エラーメッセージのカスタマイズ
+ *
+ * @return array
+ */
+ public function messages(): array
+ {
+ return [
+ 'current_password.required' => '当前パスワードを入力してください。',
+ 'current_password.min' => '当前パスワードは8文字以上です。',
+ 'current_password.max' => '当前パスワードは64文字以下です。',
+
+ 'password.required' => '新パスワードを入力してください。',
+ 'password.min' => '新パスワードは8文字以上です。',
+ 'password.max' => '新パスワードは64文字以下です。',
+ 'password.regex' => '新パスワードは英数字と記号のみ使用できます。',
+
+ 'password_confirmation.required' => '新パスワード確認を入力してください。',
+ 'password_confirmation.same' => '新パスワードと新パスワード確認は一致する必要があります。',
+ ];
+ }
+}
diff --git a/app/Mail/EmailOtpMail.php b/app/Mail/EmailOtpMail.php
new file mode 100644
index 0000000..c4c0906
--- /dev/null
+++ b/app/Mail/EmailOtpMail.php
@@ -0,0 +1,69 @@
+otpCode = $otpCode;
+ $this->operatorName = $operatorName;
+ }
+
+ /**
+ * メールのエンベロープ
+ */
+ public function envelope(): Envelope
+ {
+ return new Envelope(
+ subject: 'ログイン確認用OTPコード(有効期限:10分間)'
+ );
+ }
+
+ /**
+ * メールのコンテンツ
+ */
+ public function content(): Content
+ {
+ return new Content(
+ view: 'emails.otp',
+ with: [
+ 'otpCode' => $this->otpCode,
+ 'operatorName' => $this->operatorName,
+ ]
+ );
+ }
+
+ /**
+ * メールの添付ファイル
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Models/City.php b/app/Models/City.php
index ccfbd51..080f8d7 100644
--- a/app/Models/City.php
+++ b/app/Models/City.php
@@ -16,6 +16,7 @@ class City extends Model
'print_layout',
'city_user',
'city_remarks',
+ 'management_id',
'created_at',
'updated_at',
];
@@ -26,11 +27,40 @@ class City extends Model
public static function getList(?int $operatorId = null): array
{
return static::query()
- ->when($operatorId, fn ($q) => $q->where('operator_id', $operatorId))
+ ->when($operatorId, fn($q) => $q->where('operator_id', $operatorId))
->orderBy('city_name')
->pluck('city_name', 'city_id')
->toArray();
}
-
-
-}
+
+ /**
+ * この都市が属する運営元を取得
+ */
+ public function management(): \Illuminate\Database\Eloquent\Relations\BelongsTo
+ {
+ return $this->belongsTo(Management::class, 'management_id', 'management_id');
+ }
+
+ /**
+ * 自治体別ダッシュボード画面の表示
+ *
+ * 指定された city_id に基づいて都市情報を取得し、
+ * 該当データが存在しない場合は 404 エラーを返します。
+ * 正常に取得できた場合は、ダッシュボード画面を表示します。
+ *
+ * @param int $city_id 都市ID
+ * @return \Illuminate\View\View
+ */
+ public function dashboard($city_id)
+ {
+ $city = City::find($city_id);
+ if (!$city) {
+ abort(404);
+ }
+
+ // ここに自治体別ダッシュボードの処理を書く
+ return view('admin.CityMaster.dashboard', [
+ 'city' => $city,
+ ]);
+ }
+}
diff --git a/app/Models/Feature.php b/app/Models/Feature.php
new file mode 100644
index 0000000..a1cb981
--- /dev/null
+++ b/app/Models/Feature.php
@@ -0,0 +1,23 @@
+belongsTo(Management::class, 'management_id', 'management_id');
+ }
}
\ No newline at end of file
diff --git a/app/Models/OpePermission.php b/app/Models/OpePermission.php
new file mode 100644
index 0000000..2382ac2
--- /dev/null
+++ b/app/Models/OpePermission.php
@@ -0,0 +1,65 @@
+where('municipality_id', $municipalityId)
+ ->where('feature_id', $featureId)
+ ->delete();
+
+ // ※新規追加
+ $permissionIds = array_values(array_unique(array_map('intval', $permissionIds)));
+ foreach ($permissionIds as $pid) {
+ self::create([
+ 'municipality_id' => $municipalityId,
+ 'feature_id' => $featureId,
+ 'permission_id' => $pid,
+ ]);
+ }
+ }
+
+ /**
+ * 付与済み権限ID一覧を取得(自治体単位)
+ */
+ public static function getPermissionIds(
+ int $municipalityId,
+ int $featureId
+ ): array {
+ return self::query()
+ ->where('municipality_id', $municipalityId)
+ ->where('feature_id', $featureId)
+ ->pluck('permission_id')
+ ->map(fn ($v) => (int)$v)
+ ->toArray();
+ }
+}
diff --git a/app/Models/ParkingRegulation.php b/app/Models/ParkingRegulation.php
new file mode 100644
index 0000000..60308a1
--- /dev/null
+++ b/app/Models/ParkingRegulation.php
@@ -0,0 +1,30 @@
+where('code', $code)->first(['id']);
+ return $row?->id;
+ }
+}
diff --git a/app/Models/ReductionMaster.php b/app/Models/ReductionMaster.php
new file mode 100644
index 0000000..e199364
--- /dev/null
+++ b/app/Models/ReductionMaster.php
@@ -0,0 +1,48 @@
+where('user_categoryid', $userCategoryId)
+ ->first();
+ }
+
+ /**
+ * レコード保存時に operator_id を自動設定
+ */
+ public static function boot()
+ {
+ parent::boot();
+ self::saving(function (ReductionMaster $model) {
+ if (!isset($model->operator_id) || $model->operator_id === null) {
+ $model->operator_id = Auth::user()->ope_id ?? null;
+ }
+ });
+ }
+}
diff --git a/app/Models/management.php b/app/Models/management.php
new file mode 100644
index 0000000..33b71dd
--- /dev/null
+++ b/app/Models/management.php
@@ -0,0 +1,62 @@
+ 'integer',
+ 'municipality_flag' => 'integer',
+ 'government_approval_required' => 'integer',
+ 'created_at' => 'datetime',
+ 'updated_at' => 'datetime',
+ ];
+
+ /** 自治体かどうかを判定 */
+ public function isMunicipality(): bool
+ {
+ return (int)$this->municipality_flag === 1;
+ }
+
+ /** 役所承認が必要かどうか */
+ public function requiresGovernmentApproval(): bool
+ {
+ return (int)$this->government_approval_required === 1;
+ }
+
+ /**
+ * この運営元に属する自治体を取得
+ */
+ public function cities(): \Illuminate\Database\Eloquent\Relations\HasMany
+ {
+ return $this->hasMany(City::class, 'management_id', 'management_id');
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 8d51665..85279e3 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -9,6 +9,7 @@ use App\Services\ShjNineService;
use App\Services\ShjTenService;
use App\Services\ShjSixService;
use Illuminate\Support\Facades\DB;
+use App\Services\MenuAccessService;
class AppServiceProvider extends ServiceProvider
{
@@ -109,10 +110,15 @@ class AppServiceProvider extends ServiceProvider
->limit(5)
->get();
+ $menuAccessService = app(MenuAccessService::class);
+ $isSorin = $menuAccessService->isSorin();
+ $visibleCities = $menuAccessService->visibleCities();
+
$view->with(compact(
'taskCount','taskLatest',
'hardCount','hardLatest',
- 'latestTasks','latestHards'
+ 'latestTasks','latestHards',
+ 'isSorin', 'visibleCities'
));
});
}
diff --git a/app/Services/EmailOtpService.php b/app/Services/EmailOtpService.php
new file mode 100644
index 0000000..6ecf05f
--- /dev/null
+++ b/app/Services/EmailOtpService.php
@@ -0,0 +1,148 @@
+email_otp_code_hash = Hash::make($plainCode);
+ $ope->email_otp_expires_at = now()->addMinutes(self::OTP_VALIDITY_MINUTES);
+ $ope->email_otp_last_sent_at = now();
+ $ope->save();
+
+ return $plainCode;
+ }
+
+ /**
+ * OTP コードを検証
+ *
+ * 入力されたコードが正しく、且つ有効期限内かを確認します
+ *
+ * @param Ope $ope オペレータモデル
+ * @param string $inputCode ユーザーが入力したコード
+ * @return bool 検証結果
+ */
+ public function verify(Ope $ope, string $inputCode): bool
+ {
+ // 有効期限チェック
+ if (!$ope->email_otp_expires_at || $ope->email_otp_expires_at < now()) {
+ return false;
+ }
+
+ // hash化されたコードと比較
+ if (!Hash::check($inputCode, $ope->email_otp_code_hash)) {
+ return false;
+ }
+
+ // 検証成功:検証完了時刻を記録し、OTPコードをクリア
+ $ope->email_otp_verified_at = now();
+ $ope->email_otp_code_hash = null;
+ $ope->email_otp_expires_at = null;
+ $ope->save();
+
+ return true;
+ }
+
+ /**
+ * OTP が最近検証されたかを確認(24時間以内)
+ *
+ * @param Ope $ope オペレータモデル
+ * @return bool true: 24時間以内に検証済み、false: 未検証または24時間以上経過
+ */
+ public function isOtpRecent(Ope $ope): bool
+ {
+ if (!$ope->email_otp_verified_at) {
+ return false;
+ }
+
+ return $ope->email_otp_verified_at > now()->subHours(self::OTP_FREE_PERIOD_HOURS);
+ }
+
+ /**
+ * 重発可能かを確認
+ *
+ * 前回送信から60秒以上経過しているかを確認します
+ *
+ * @param Ope $ope オペレータモデル
+ * @return bool true: 重発可能、false: 待機中
+ */
+ public function canResend(Ope $ope): bool
+ {
+ if (!$ope->email_otp_last_sent_at) {
+ return true;
+ }
+
+ return $ope->email_otp_last_sent_at <= now()->subSeconds(self::OTP_RESEND_DELAY_SECONDS);
+ }
+
+ /**
+ * 次の重発までの待機時間を取得(秒)
+ *
+ * @param Ope $ope オペレータモデル
+ * @return int 待機時間(秒)、0以下は重発可能
+ */
+ public function getResendWaitSeconds(Ope $ope): int
+ {
+ if (!$ope->email_otp_last_sent_at) {
+ return 0;
+ }
+
+ $diffSeconds = now()->diffInSeconds($ope->email_otp_last_sent_at, absolute: false);
+ $waitSeconds = self::OTP_RESEND_DELAY_SECONDS - abs($diffSeconds);
+
+ return max(0, $waitSeconds);
+ }
+
+ /**
+ * OTP コードのマスク済みメールアドレスを取得
+ *
+ * abc@example.com -> abc***@example.com のようなマスク表示用
+ *
+ * @param string $email メールアドレス
+ * @return string マスク済みメールアドレス
+ */
+ public function maskEmail(string $email): string
+ {
+ [$name, $domain] = explode('@', $email);
+
+ // 名前部分の最初の1文字を残して、残りは*でマスク
+ $maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 1));
+
+ return "{$maskedName}@{$domain}";
+ }
+}
diff --git a/app/Services/MenuAccessService.php b/app/Services/MenuAccessService.php
new file mode 100644
index 0000000..74e5557
--- /dev/null
+++ b/app/Services/MenuAccessService.php
@@ -0,0 +1,133 @@
+getAuthOperator();
+ if (!$operator || !isset($operator->management_id)) {
+ return null;
+ }
+
+ return Management::find($operator->management_id);
+ }
+
+ /**
+ * ログイン中のオペレータがソーリンであるか判定
+ *
+ * ソーリン(management_name === 'ソーリン')の場合は全メニュー表示可能
+ *
+ * @return bool true: ソーリン, false: その他
+ */
+ public function isSorin(): bool
+ {
+ $management = $this->getAuthManagement();
+ if (!$management) {
+ return false;
+ }
+
+ return $management->management_name === 'ソーリン';
+ }
+
+ /**
+ * オペレータが表示可能な自治体リストを取得
+ *
+ * ソーリン: 全自治体(削除フラグなどは既存仕様に合わせる)
+ * その他: ope.city_id で指定された1つの自治体のみ
+ *
+ * @return Collection
+ */
+ public function visibleCities(): Collection
+ {
+ if ($this->isSorin()) {
+ return City::query()
+ ->orderBy('city_name')
+ ->get();
+ }
+
+ // 非ソーリン時は ope.city_id で指定された自治体のみ
+ $operator = $this->getAuthOperator();
+ if (!$operator || !isset($operator->city_id)) {
+ return collect();
+ }
+
+ // ope.city_id に一致する city のみを返す
+ $city = City::find($operator->city_id);
+ if (!$city) {
+ return collect();
+ }
+
+ return collect([$city]);
+ }
+
+ /**
+ * 指定された自治体へのアクセスが許可されているか判定
+ *
+ * ソーリン: 常に true
+ * その他: city.management_id == ope.management_id の場合のみ true
+ *
+ * @param int $cityId 自治体ID
+ * @return bool true: アクセス許可, false: アクセス拒否
+ */
+ public function canAccessCity(int $cityId): bool
+ {
+ if ($this->isSorin()) {
+ return true;
+ }
+
+ $operator = $this->getAuthOperator();
+ if (!$operator || !isset($operator->management_id)) {
+ return false;
+ }
+
+ // city.management_id が ope.management_id と一致するか確認
+ $city = City::find($cityId);
+ if (!$city) {
+ return false;
+ }
+
+ return (int)$city->management_id === (int)$operator->management_id;
+ }
+
+ /**
+ * 複数の自治体へのアクセス許可を一括確認
+ *
+ * @param array $cityIds 自治体IDの配列
+ * @return bool すべての自治体へのアクセスが許可されている場合のみ true
+ */
+ public function canAccessCities(array $cityIds): bool
+ {
+ foreach ($cityIds as $cityId) {
+ if (!$this->canAccessCity($cityId)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index 8a80778..52ac71d 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -17,6 +17,16 @@ return Application::configure(basePath: dirname(__DIR__))
'/shj4a', // SHJ-4A本番用エンドポイント
'/webhook/wellnet', // SHJ-4A開発・デバッグ用エンドポイント
]);
+
+ // グローバルミドルウェア登録(すべてのリクエストに適用)
+ $middleware->append(\App\Http\Middleware\ShareMenuAccessData::class);
+
+ // ミドルウェアエイリアス登録
+ $middleware->alias([
+ 'check.city.access' => \App\Http\Middleware\CheckCityAccess::class,
+ 'ensure.otp.verified' => \App\Http\Middleware\EnsureOtpVerified::class,
+ 'check.password.change.required' => \App\Http\Middleware\CheckPasswordChangeRequired::class,
+ ]);
})
->withExceptions(function (Exceptions $exceptions) {
//
diff --git a/config/view.php b/config/view.php
new file mode 100644
index 0000000..943eeca
--- /dev/null
+++ b/config/view.php
@@ -0,0 +1,50 @@
+ [
+ resource_path('views'),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Compiled View Path
+ |--------------------------------------------------------------------------
+ |
+ | This option determines where all the compiled Blade templates will be
+ | stored for your application. Typically, this is within the storage
+ | directory. However, as usual, you are free to change this value.
+ |
+ */
+
+ 'compiled' => env(
+ 'VIEW_COMPILED_PATH',
+ realpath(storage_path('framework/views'))
+ ),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Blade Namespace Paths
+ |--------------------------------------------------------------------------
+ |
+ | Define custom namespace paths for views. This allows you to organize
+ | views into different directories and reference them using namespace syntax.
+ |
+ */
+
+ 'namespaces' => [
+ 'mail' => resource_path('views/emails'),
+ ],
+
+];
diff --git a/public/plugins/summernote/font/summernote.ttf b/public/plugins/summernote/font/summernote.ttf
new file mode 100644
index 0000000..b8266cd
Binary files /dev/null and b/public/plugins/summernote/font/summernote.ttf differ
diff --git a/public/plugins/summernote/font/summernote.woff2 b/public/plugins/summernote/font/summernote.woff2
new file mode 100644
index 0000000..d3a8164
Binary files /dev/null and b/public/plugins/summernote/font/summernote.woff2 differ
diff --git a/public/plugins/summernote/summernote.min.css b/public/plugins/summernote/summernote.min.css
new file mode 100644
index 0000000..3a70f52
--- /dev/null
+++ b/public/plugins/summernote/summernote.min.css
@@ -0,0 +1 @@
+@font-face{font-display:auto;font-family:summernote;font-style:normal;font-weight:400;src:url(font/summernote.eot?#iefix) format("embedded-opentype"),url(font/summernote.woff2) format("woff2"),url(font/summernote.woff) format("woff"),url(font/summernote.ttf) format("truetype")}[class*=" note-icon"]:before,[class^=note-icon]:before{display:inline-block;font-family:summernote;font-size:inherit;font-style:normal;text-decoration:inherit;text-rendering:auto;text-transform:none;vertical-align:middle;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;speak:none}.note-icon-fw{text-align:center;width:1.25em}.note-icon-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.note-icon-pull-left{float:left}.note-icon-pull-right{float:right}.note-icon.note-icon-pull-left{margin-right:.3em}.note-icon.note-icon-pull-right{margin-left:.3em}.note-icon-align:before{content:"\ea01"}.note-icon-align-center:before{content:"\ea02"}.note-icon-align-indent:before{content:"\ea03"}.note-icon-align-justify:before{content:"\ea04"}.note-icon-align-left:before{content:"\ea05"}.note-icon-align-outdent:before{content:"\ea06"}.note-icon-align-right:before{content:"\ea07"}.note-icon-arrow-circle-down:before{content:"\ea08"}.note-icon-arrow-circle-left:before{content:"\ea09"}.note-icon-arrow-circle-right:before{content:"\ea0a"}.note-icon-arrow-circle-up:before{content:"\ea0b"}.note-icon-arrows-alt:before{content:"\ea0c"}.note-icon-arrows-h:before{content:"\ea0d"}.note-icon-arrows-v:before{content:"\ea0e"}.note-icon-bold:before{content:"\ea0f"}.note-icon-caret:before{content:"\ea10"}.note-icon-chain-broken:before{content:"\ea11"}.note-icon-circle:before{content:"\ea12"}.note-icon-close:before{content:"\ea13"}.note-icon-code:before{content:"\ea14"}.note-icon-col-after:before{content:"\ea15"}.note-icon-col-before:before{content:"\ea16"}.note-icon-col-remove:before{content:"\ea17"}.note-icon-eraser:before{content:"\ea18"}.note-icon-float-left:before{content:"\ea19"}.note-icon-float-none:before{content:"\ea1a"}.note-icon-float-right:before{content:"\ea1b"}.note-icon-font:before{content:"\ea1c"}.note-icon-frame:before{content:"\ea1d"}.note-icon-italic:before{content:"\ea1e"}.note-icon-link:before{content:"\ea1f"}.note-icon-magic:before{content:"\ea20"}.note-icon-menu-check:before{content:"\ea21"}.note-icon-minus:before{content:"\ea22"}.note-icon-orderedlist:before{content:"\ea23"}.note-icon-pencil:before{content:"\ea24"}.note-icon-picture:before{content:"\ea25"}.note-icon-question:before{content:"\ea26"}.note-icon-redo:before{content:"\ea27"}.note-icon-rollback:before{content:"\ea28"}.note-icon-row-above:before{content:"\ea29"}.note-icon-row-below:before{content:"\ea2a"}.note-icon-row-remove:before{content:"\ea2b"}.note-icon-special-character:before{content:"\ea2c"}.note-icon-square:before{content:"\ea2d"}.note-icon-strikethrough:before{content:"\ea2e"}.note-icon-subscript:before{content:"\ea2f"}.note-icon-summernote:before{content:"\ea30"}.note-icon-superscript:before{content:"\ea31"}.note-icon-table:before{content:"\ea32"}.note-icon-text-height:before{content:"\ea33"}.note-icon-trash:before{content:"\ea34"}.note-icon-underline:before{content:"\ea35"}.note-icon-undo:before{content:"\ea36"}.note-icon-unorderedlist:before{content:"\ea37"}.note-icon-video:before{content:"\ea38"}.note-editor{position:relative}.note-editor .note-dropzone{background-color:#fff;color:#87cefa;display:none;opacity:.95;position:absolute;z-index:100}.note-editor .note-dropzone .note-dropzone-message{display:table-cell;font-size:28px;font-weight:700;text-align:center;vertical-align:middle}.note-editor .note-dropzone.hover{color:#098ddf}.note-editor.dragover .note-dropzone{display:table}.note-editor .note-editing-area{position:relative}.note-editor .note-editing-area .note-editable{outline:none}.note-editor .note-editing-area .note-editable sup{vertical-align:super}.note-editor .note-editing-area .note-editable sub{vertical-align:sub}.note-editor .note-editing-area .note-editable img.note-float-left{margin-right:10px}.note-editor .note-editing-area .note-editable img.note-float-right{margin-left:10px}.note-editor.note-airframe,.note-editor.note-frame{border:1px solid rgba(0,0,0,.196)}.note-editor.note-airframe.codeview .note-editing-area .note-editable,.note-editor.note-frame.codeview .note-editing-area .note-editable{display:none}.note-editor.note-airframe.codeview .note-editing-area .note-codable,.note-editor.note-frame.codeview .note-editing-area .note-codable{display:block}.note-editor.note-airframe .note-editing-area,.note-editor.note-frame .note-editing-area{overflow:hidden}.note-editor.note-airframe .note-editing-area .note-editable,.note-editor.note-frame .note-editing-area .note-editable{overflow:auto;padding:10px;word-wrap:break-word}.note-editor.note-airframe .note-editing-area .note-editable[contenteditable=false],.note-editor.note-frame .note-editing-area .note-editable[contenteditable=false]{background-color:hsla(0,0%,50%,.114)}.note-editor.note-airframe .note-editing-area .note-codable,.note-editor.note-frame .note-editing-area .note-codable{background-color:#222;border:none;border-radius:0;box-shadow:none;-ms-box-sizing:border-box;box-sizing:border-box;color:#ccc;display:none;font-family:Menlo,Monaco,monospace,sans-serif;font-size:14px;margin-bottom:0;outline:none;padding:10px;resize:none;width:100%}.note-editor.note-airframe.fullscreen,.note-editor.note-frame.fullscreen{left:0;position:fixed;top:0;width:100%!important;z-index:1050}.note-editor.note-airframe.fullscreen .note-resizebar,.note-editor.note-frame.fullscreen .note-resizebar{display:none}.note-editor.note-airframe .note-status-output,.note-editor.note-frame .note-status-output{border:0;border-top:1px solid #e2e2e2;color:#000;display:block;font-size:14px;height:20px;line-height:1.42857143;margin-bottom:0;width:100%}.note-editor.note-airframe .note-status-output:empty,.note-editor.note-frame .note-status-output:empty{border-top:0 solid transparent;height:0}.note-editor.note-airframe .note-status-output .pull-right,.note-editor.note-frame .note-status-output .pull-right{float:right!important}.note-editor.note-airframe .note-status-output .text-muted,.note-editor.note-frame .note-status-output .text-muted{color:#777}.note-editor.note-airframe .note-status-output .text-primary,.note-editor.note-frame .note-status-output .text-primary{color:#286090}.note-editor.note-airframe .note-status-output .text-success,.note-editor.note-frame .note-status-output .text-success{color:#3c763d}.note-editor.note-airframe .note-status-output .text-info,.note-editor.note-frame .note-status-output .text-info{color:#31708f}.note-editor.note-airframe .note-status-output .text-warning,.note-editor.note-frame .note-status-output .text-warning{color:#8a6d3b}.note-editor.note-airframe .note-status-output .text-danger,.note-editor.note-frame .note-status-output .text-danger{color:#a94442}.note-editor.note-airframe .note-status-output .alert,.note-editor.note-frame .note-status-output .alert{background-color:#f5f5f5;border-radius:0;color:#000;margin:-7px 0 0;padding:7px 10px 2px}.note-editor.note-airframe .note-status-output .alert .note-icon,.note-editor.note-frame .note-status-output .alert .note-icon{margin-right:5px}.note-editor.note-airframe .note-status-output .alert-success,.note-editor.note-frame .note-status-output .alert-success{background-color:#dff0d8!important;color:#3c763d!important}.note-editor.note-airframe .note-status-output .alert-info,.note-editor.note-frame .note-status-output .alert-info{background-color:#d9edf7!important;color:#31708f!important}.note-editor.note-airframe .note-status-output .alert-warning,.note-editor.note-frame .note-status-output .alert-warning{background-color:#fcf8e3!important;color:#8a6d3b!important}.note-editor.note-airframe .note-status-output .alert-danger,.note-editor.note-frame .note-status-output .alert-danger{background-color:#f2dede!important;color:#a94442!important}.note-editor.note-airframe .note-statusbar,.note-editor.note-frame .note-statusbar{background-color:hsla(0,0%,50%,.114);border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-top:1px solid rgba(0,0,0,.196)}.note-editor.note-airframe .note-statusbar .note-resizebar,.note-editor.note-frame .note-statusbar .note-resizebar{cursor:ns-resize;height:9px;padding-top:1px;width:100%}.note-editor.note-airframe .note-statusbar .note-resizebar .note-icon-bar,.note-editor.note-frame .note-statusbar .note-resizebar .note-icon-bar{border-top:1px solid rgba(0,0,0,.196);margin:1px auto;width:20px}.note-editor.note-airframe .note-statusbar.locked .note-resizebar,.note-editor.note-frame .note-statusbar.locked .note-resizebar{cursor:default}.note-editor.note-airframe .note-statusbar.locked .note-resizebar .note-icon-bar,.note-editor.note-frame .note-statusbar.locked .note-resizebar .note-icon-bar{display:none}.note-editor.note-airframe .note-placeholder,.note-editor.note-frame .note-placeholder{padding:10px}.note-editor.note-airframe{border:0}.note-editor.note-airframe .note-editing-area .note-editable{padding:0}.note-popover.popover{display:none;max-width:none}.note-popover.popover .popover-content a{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;white-space:nowrap}.note-popover.popover .arrow{left:20px!important}.note-toolbar{position:relative}.note-editor .note-toolbar,.note-popover .popover-content{margin:0;padding:0 0 5px 5px}.note-editor .note-toolbar>.note-btn-group,.note-popover .popover-content>.note-btn-group{margin-left:0;margin-right:5px;margin-top:5px}.note-editor .note-toolbar .note-btn-group .note-table,.note-popover .popover-content .note-btn-group .note-table{min-width:0;padding:5px}.note-editor .note-toolbar .note-btn-group .note-table .note-dimension-picker,.note-popover .popover-content .note-btn-group .note-table .note-dimension-picker{font-size:18px}.note-editor .note-toolbar .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher,.note-popover .popover-content .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-mousecatcher{cursor:pointer;height:10em;position:absolute!important;width:10em;z-index:3}.note-editor .note-toolbar .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted,.note-popover .popover-content .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-unhighlighted{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC") repeat;height:5em;position:relative!important;width:5em;z-index:1}.note-editor .note-toolbar .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted,.note-popover .popover-content .note-btn-group .note-table .note-dimension-picker .note-dimension-picker-highlighted{background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC") repeat;height:1em;position:absolute!important;width:1em;z-index:2}.note-editor .note-toolbar .note-style .dropdown-style blockquote,.note-editor .note-toolbar .note-style .dropdown-style pre,.note-popover .popover-content .note-style .dropdown-style blockquote,.note-popover .popover-content .note-style .dropdown-style pre{margin:0;padding:5px 10px}.note-editor .note-toolbar .note-style .dropdown-style h1,.note-editor .note-toolbar .note-style .dropdown-style h2,.note-editor .note-toolbar .note-style .dropdown-style h3,.note-editor .note-toolbar .note-style .dropdown-style h4,.note-editor .note-toolbar .note-style .dropdown-style h5,.note-editor .note-toolbar .note-style .dropdown-style h6,.note-editor .note-toolbar .note-style .dropdown-style p,.note-popover .popover-content .note-style .dropdown-style h1,.note-popover .popover-content .note-style .dropdown-style h2,.note-popover .popover-content .note-style .dropdown-style h3,.note-popover .popover-content .note-style .dropdown-style h4,.note-popover .popover-content .note-style .dropdown-style h5,.note-popover .popover-content .note-style .dropdown-style h6,.note-popover .popover-content .note-style .dropdown-style p{margin:0;padding:0}.note-editor .note-toolbar .note-color-all .note-dropdown-menu,.note-popover .popover-content .note-color-all .note-dropdown-menu{min-width:337px}.note-editor .note-toolbar .note-color .dropdown-toggle,.note-popover .popover-content .note-color .dropdown-toggle{padding-left:5px;width:20px}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette{display:inline-block;margin:0;width:160px}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette:first-child,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette:first-child{margin:0 5px}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-palette-title,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-palette-title{border-bottom:1px solid #eee;font-size:12px;margin:2px 7px;text-align:center}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-reset,.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-select,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-reset,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-select{border-radius:5px;cursor:pointer;font-size:11px;margin:3px;padding:0 3px;width:100%}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-reset:hover,.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-select:hover,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-reset:hover,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-select:hover{background:#eee}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-row,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-row{height:20px}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-select-btn,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-color-select-btn{display:none}.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-holder-custom .note-color-btn,.note-popover .popover-content .note-color .note-dropdown-menu .note-palette .note-holder-custom .note-color-btn{border:1px solid #eee}.note-editor .note-toolbar .note-para .note-dropdown-menu,.note-popover .popover-content .note-para .note-dropdown-menu{min-width:228px;padding:5px}.note-editor .note-toolbar .note-para .note-dropdown-menu>div+div,.note-popover .popover-content .note-para .note-dropdown-menu>div+div{margin-left:5px}.note-editor .note-toolbar .note-dropdown-menu,.note-popover .popover-content .note-dropdown-menu{min-width:160px}.note-editor .note-toolbar .note-dropdown-menu.right,.note-popover .popover-content .note-dropdown-menu.right{left:auto;right:0}.note-editor .note-toolbar .note-dropdown-menu.right:before,.note-popover .popover-content .note-dropdown-menu.right:before{left:auto!important;right:9px}.note-editor .note-toolbar .note-dropdown-menu.right:after,.note-popover .popover-content .note-dropdown-menu.right:after{left:auto!important;right:10px}.note-editor .note-toolbar .note-dropdown-menu.note-check a i,.note-popover .popover-content .note-dropdown-menu.note-check a i{color:#00bfff;visibility:hidden}.note-editor .note-toolbar .note-dropdown-menu.note-check a.checked i,.note-popover .popover-content .note-dropdown-menu.note-check a.checked i{visibility:visible}.note-editor .note-toolbar .note-fontsize-10,.note-popover .popover-content .note-fontsize-10{font-size:10px}.note-editor .note-toolbar .note-color-palette,.note-popover .popover-content .note-color-palette{line-height:1}.note-editor .note-toolbar .note-color-palette div .note-color-btn,.note-popover .popover-content .note-color-palette div .note-color-btn{border:0;border-radius:0;height:20px;margin:0;padding:0;width:20px}.note-editor .note-toolbar .note-color-palette div .note-color-btn:hover,.note-popover .popover-content .note-color-palette div .note-color-btn:hover{transform:scale(1.2);transition:all .2s}.note-modal .modal-dialog{border-radius:5px;box-shadow:0 3px 9px rgba(0,0,0,.5);outline:0}.note-modal .form-group{margin-left:0;margin-right:0}.note-modal .note-modal-form{margin:0}.note-modal .note-image-dialog .note-dropzone{border:4px dashed #d3d3d3;color:#d3d3d3;font-size:30px;line-height:4;margin-bottom:10px;min-height:100px;text-align:center}@-moz-document url-prefix(){.note-modal .note-image-input{height:auto}}.note-placeholder{color:gray;display:none;position:absolute}.note-handle .note-control-selection{border:1px solid #000;display:none;position:absolute}.note-handle .note-control-selection>div{position:absolute}.note-handle .note-control-selection .note-control-selection-bg{background-color:#000;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(opacity=30);filter:alpha(opacity=30);height:100%;-webkit-opacity:.3;-khtml-opacity:.3;-moz-opacity:.3;opacity:.3;width:100%}.note-handle .note-control-selection .note-control-handle,.note-handle .note-control-selection .note-control-holder,.note-handle .note-control-selection .note-control-sizing{border:1px solid #000;height:7px;width:7px}.note-handle .note-control-selection .note-control-sizing{background-color:#000}.note-handle .note-control-selection .note-control-nw{border-bottom:none;border-right:none;left:-5px;top:-5px}.note-handle .note-control-selection .note-control-ne{border-bottom:none;border-left:none;right:-5px;top:-5px}.note-handle .note-control-selection .note-control-sw{border-right:none;border-top:none;bottom:-5px;left:-5px}.note-handle .note-control-selection .note-control-se{bottom:-5px;cursor:se-resize;right:-5px}.note-handle .note-control-selection .note-control-se.note-control-holder{border-left:none;border-top:none;cursor:default}.note-handle .note-control-selection .note-control-selection-info{background-color:#000;border-radius:5px;bottom:0;color:#fff;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(opacity=70);filter:alpha(opacity=70);font-size:12px;margin:5px;-webkit-opacity:.7;-khtml-opacity:.7;-moz-opacity:.7;opacity:.7;padding:5px;right:0}.note-hint-popover{min-width:100px;padding:2px}.note-hint-popover .popover-content{max-height:150px;overflow:auto;padding:3px}.note-hint-popover .popover-content .note-hint-group .note-hint-item{display:block!important;padding:3px}.note-hint-popover .popover-content .note-hint-group .note-hint-item.active,.note-hint-popover .popover-content .note-hint-group .note-hint-item:hover{background-color:#428bca;clear:both;color:#fff;cursor:pointer;display:block;font-weight:400;line-height:1.4;outline:0;text-decoration:none;white-space:nowrap}body .note-fullscreen-body,html .note-fullscreen-body{overflow:hidden!important}.note-editable ol li,.note-editable ul li{list-style-position:inside}
\ No newline at end of file
diff --git a/public/plugins/summernote/summernote.min.js b/public/plugins/summernote/summernote.min.js
new file mode 100644
index 0000000..52e140e
--- /dev/null
+++ b/public/plugins/summernote/summernote.min.js
@@ -0,0 +1,2 @@
+/*! Summernote v0.9.0 | (c) 2013~ Hackerwins and contributors | MIT license */
+!function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e(require("jquery"));else if("function"==typeof define&&define.amd)define(["jquery"],e);else{var o="object"==typeof exports?e(require("jquery")):e(t.jQuery);for(var n in o)("object"==typeof exports?exports:t)[n]=o[n]}}(self,(t=>(()=>{"use strict";var e={7e3:(t,e,o)=>{var n=o(8938),i=o.n(n);i().summernote=i().summernote||{lang:{}},i().extend(!0,i().summernote.lang,{"en-US":{font:{bold:"Bold",italic:"Italic",underline:"Underline",clear:"Remove Font Style",height:"Line Height",name:"Font Family",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript",size:"Font Size",sizeunit:"Font Size Unit"},image:{image:"Picture",insert:"Insert Image",resizeFull:"Resize full",resizeHalf:"Resize half",resizeQuarter:"Resize quarter",resizeNone:"Original size",floatLeft:"Float Left",floatRight:"Float Right",floatNone:"Remove float",shapeRounded:"Shape: Rounded",shapeCircle:"Shape: Circle",shapeThumbnail:"Shape: Thumbnail",shapeNone:"Shape: None",dragImageHere:"Drag image or text here",dropImage:"Drop image or Text",selectFromFiles:"Select from files",maximumFileSize:"Maximum file size",maximumFileSizeError:"Maximum file size exceeded.",url:"Image URL",remove:"Remove Image",original:"Original"},video:{video:"Video",videoLink:"Video Link",insert:"Insert Video",url:"Video URL",providers:"(YouTube, Google Drive, Vimeo, Vine, Instagram, DailyMotion, Youku, Peertube)"},link:{link:"Link",insert:"Insert Link",unlink:"Unlink",edit:"Edit",textToDisplay:"Text to display",url:"To what URL should this link go?",openInNewWindow:"Open in new window"},table:{table:"Table",addRowAbove:"Add row above",addRowBelow:"Add row below",addColLeft:"Add column left",addColRight:"Add column right",delRow:"Delete row",delCol:"Delete column",delTable:"Delete table"},hr:{insert:"Insert Horizontal Rule"},style:{style:"Style",p:"Normal",blockquote:"Quote",pre:"Code",h1:"Header 1",h2:"Header 2",h3:"Header 3",h4:"Header 4",h5:"Header 5",h6:"Header 6"},lists:{unordered:"Unordered list",ordered:"Ordered list"},options:{help:"Help",fullscreen:"Full Screen",codeview:"Code View"},paragraph:{paragraph:"Paragraph",outdent:"Outdent",indent:"Indent",left:"Align left",center:"Align center",right:"Align right",justify:"Justify full"},color:{recent:"Recent Color",more:"More Color",background:"Background Color",foreground:"Text Color",transparent:"Transparent",setTransparent:"Set transparent",reset:"Reset",resetToDefault:"Reset to default",cpSelect:"Select"},shortcut:{shortcuts:"Keyboard shortcuts",close:"Close",textFormatting:"Text formatting",action:"Action",paragraphFormatting:"Paragraph formatting",documentStyle:"Document Style",extraKeys:"Extra keys"},help:{escape:"Escape",insertParagraph:"Insert Paragraph",undo:"Undo the last command",redo:"Redo the last command",tab:"Tab",untab:"Untab",bold:"Set a bold style",italic:"Set a italic style",underline:"Set a underline style",strikethrough:"Set a strikethrough style",removeFormat:"Clean a style",justifyLeft:"Set left align",justifyCenter:"Set center align",justifyRight:"Set right align",justifyFull:"Set full align",insertUnorderedList:"Toggle unordered list",insertOrderedList:"Toggle ordered list",outdent:"Outdent on current paragraph",indent:"Indent on current paragraph",formatPara:"Change current block's format as a paragraph(P tag)",formatH1:"Change current block's format as H1",formatH2:"Change current block's format as H2",formatH3:"Change current block's format as H3",formatH4:"Change current block's format as H4",formatH5:"Change current block's format as H5",formatH6:"Change current block's format as H6",insertHorizontalRule:"Insert horizontal rule","linkDialog.show":"Show Link Dialog"},history:{undo:"Undo",redo:"Redo"},specialChar:{specialChar:"SPECIAL CHARACTERS",select:"Select Special characters"},output:{noSelection:"No Selection Made!"}}})},8938:e=>{e.exports=t}},o={};function n(t){var i=o[t];if(void 0!==i)return i.exports;var r=o[t]={exports:{}};return e[t](r,r.exports,n),r.exports}n.n=t=>{var e=t&&t.__esModule?()=>t.default:()=>t;return n.d(e,{a:e}),e},n.d=(t,e)=>{for(var o in e)n.o(e,o)&&!n.o(t,o)&&Object.defineProperty(t,o,{enumerable:!0,get:e[o]})},n.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);var i=n(8938),r=n.n(i),a=(n(7e3),["sans-serif","serif","monospace","cursive","fantasy"]);function s(t){return-1===r().inArray(t.toLowerCase(),a)?"'".concat(t,"'"):t}var l,c=navigator.userAgent,u=/MSIE|Trident/i.test(c);if(u){var d=/MSIE (\d+[.]\d+)/.exec(c);d&&(l=parseFloat(d[1])),(d=/Trident\/.*rv:([0-9]{1,}[.0-9]{0,})/.exec(c))&&(l=parseFloat(d[1]))}var f=/Edge\/\d+/.test(c),h="ontouchstart"in window||navigator.MaxTouchPoints>0||navigator.msMaxTouchPoints>0,p=u?"DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted":"input";const m={isMac:navigator.appVersion.indexOf("Mac")>-1,isMSIE:u,isEdge:f,isFF:!f&&/firefox/i.test(c),isPhantom:/PhantomJS/i.test(c),isWebkit:!f&&/webkit/i.test(c),isChrome:!f&&/chrome/i.test(c),isSafari:!f&&/safari/i.test(c)&&!/chrome/i.test(c),browserVersion:l,isSupportTouch:h,isFontInstalled:function(){var t=document.createElement("canvas"),e=t.getContext("2d",{willReadFrequently:!0});function o(t,o){return e.clearRect(0,0,40,20),e.font="20px "+s(t)+', "'+o+'"',e.fillText("mw",20,10),e.getImageData(0,0,40,20).data.join("")}return t.width=40,t.height=20,e.textAlign="center",e.fillStyle="black",e.textBaseline="middle",function(t){var e="Comic Sans MS"===t?"Courier New":"Comic Sans MS";return o(e,e)!==o(t,e)}}(),isW3CRangeSupport:!!document.createRange,inputEventName:p,genericFontFamilies:a,validFontName:s};var v=0;const g={eq:function(t){return function(e){return t===e}},eq2:function(t,e){return t===e},peq2:function(t){return function(e,o){return e[t]===o[t]}},ok:function(){return!0},fail:function(){return!1},self:function(t){return t},not:function(t){return function(){return!t.apply(t,arguments)}},and:function(t,e){return function(o){return t(o)&&e(o)}},invoke:function(t,e){return function(){return t[e].apply(t,arguments)}},resetUniqueId:function(){v=0},uniqueId:function(t){var e=++v+"";return t?t+e:e},rect2bnd:function(t){var e=r()(document);return{top:t.top+e.scrollTop(),left:t.left+e.scrollLeft(),width:t.right-t.left,height:t.bottom-t.top}},invertObject:function(t){var e={};for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&(e[t[o]]=o);return e},namespaceToCamel:function(t,e){return(e=e||"")+t.split(".").map((function(t){return t.substring(0,1).toUpperCase()+t.substring(1)})).join("")},debounce:function(t,e,o){var n;return function(){var i=this,r=arguments,a=o&&!n;clearTimeout(n),n=setTimeout((function(){n=null,o||t.apply(i,r)}),e),a&&t.apply(i,r)}},isValidUrl:function(t){return/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/gi.test(t)}};function b(t){return t[0]}function y(t){return t[t.length-1]}function k(t){return t.slice(1)}function w(t,e){if(t&&t.length&&e){if(t.indexOf)return-1!==t.indexOf(e);if(t.contains)return t.contains(e)}return!1}const C={head:b,last:y,initial:function(t){return t.slice(0,t.length-1)},tail:k,prev:function(t,e){if(t&&t.length&&e){var o=t.indexOf(e);return-1===o?null:t[o-1]}return null},next:function(t,e){if(t&&t.length&&e){var o=t.indexOf(e);return-1===o?null:t[o+1]}return null},find:function(t,e){for(var o=0,n=t.length;o";function U(t){return T(t)?t.nodeValue.length:t?t.childNodes.length:0}function W(t){var e=U(t);return 0===e||(!T(t)&&1===e&&t.innerHTML===M||!(!C.all(t.childNodes,T)||""!==t.innerHTML))}function K(t){P(t)||U(t)||(t.innerHTML=M)}function q(t,e){for(;t;){if(e(t))return t;if(x(t))break;t=t.parentNode}return null}function V(t,e){e=e||g.fail;var o=[];return q(t,(function(t){return x(t)||o.push(t),e(t)})),o}function _(t,e){e=e||g.fail;for(var o=[];t&&!e(t);)o.push(t),t=t.nextSibling;return o}function G(t,e){var o=e.nextSibling,n=e.parentNode;return o?n.insertBefore(t,o):n.appendChild(t),t}function Z(t,e,o){return r().each(e,(function(e,n){!o&&$(t)&&null===t.firstChild&&F(n)&&t.appendChild(ut("br")),t.appendChild(n)})),t}function Y(t){return 0===t.offset}function X(t){return t.offset===U(t.node)}function Q(t){return Y(t)||X(t)}function J(t,e){for(;t&&t!==e;){if(0!==et(t))return!1;t=t.parentNode}return!0}function tt(t,e){if(!e)return!1;for(;t&&t!==e;){if(et(t)!==U(t.parentNode)-1)return!1;t=t.parentNode}return!0}function et(t){for(var e=0;t=t.previousSibling;)e+=1;return e}function ot(t){return!!(t&&t.childNodes&&t.childNodes.length)}function nt(t,e){var o,n;if(0===t.offset){if(x(t.node))return null;o=t.node.parentNode,n=et(t.node)}else ot(t.node)?n=U(o=t.node.childNodes[t.offset-1]):(o=t.node,n=e?0:t.offset-1);return{node:o,offset:n}}function it(t,e){var o,n;if(U(t.node)===t.offset){if(x(t.node))return null;var i=at(t.node);i?(o=i,n=0):(o=t.node.parentNode,n=et(t.node)+1)}else ot(t.node)?(o=t.node.childNodes[t.offset],n=0):(o=t.node,n=e?U(t.node):t.offset+1);return{node:o,offset:n}}function rt(t,e){var o,n=0;if(U(t.node)===t.offset){if(x(t.node))return null;o=t.node.parentNode,n=et(t.node)+1,x(o)&&(o=t.node.nextSibling,n=0)}else ot(t.node)?(o=t.node.childNodes[t.offset],n=0):(o=t.node,n=e?U(t.node):t.offset+1);return{node:o,offset:n}}function at(t){if(t.nextSibling&&t.parent===t.nextSibling.parent)return T(t.nextSibling)?t.nextSibling:at(t.nextSibling)}function st(t,e){return t.node===e.node&&t.offset===e.offset}function lt(t,e){var o=e&&e.isSkipPaddingBlankHTML,n=e&&e.isNotSplitEdgePoint,i=e&&e.isDiscardEmptySplits;if(i&&(o=!0),Q(t)&&(T(t.node)||n)){if(Y(t))return t.node;if(X(t))return t.node.nextSibling}if(T(t.node))return t.node.splitText(t.offset);var r=_(t.node.childNodes[t.offset]),a=G(t.node.cloneNode(!1),t.node);return Z(a,r),o||(K(t.node),K(a)),i&&(W(t.node)&&dt(t.node),W(a))?(dt(a),t.node.nextSibling):a}function ct(t,e,o){var n=V(e.node,g.eq(t));if(!n.length)return null;if(1===n.length)return lt(e,o);if(n.length>2){var i=n.slice(0,n.length-1).find((function(t){return t.nextSibling}));if(i&&0!=e.offset&&X(e)){var r,a=i.nextSibling;1==a.nodeType?(n=V(r=a.childNodes[0],g.eq(t)),e={node:r,offset:0}):3!=a.nodeType||a.data.match(/[\n\r]/g)||(n=V(r=a,g.eq(t)),e={node:r,offset:0})}}return n.reduce((function(t,n){return t===e.node&&(t=lt(e,o)),lt({node:n,offset:t?et(t):U(n)},o)}))}function ut(t){return document.createElement(t)}function dt(t,e){if(t&&t.parentNode){if(t.removeNode)return t.removeNode(e);var o=t.parentNode;if(!e){for(var n=[],i=0,r=t.childNodes.length;i".concat(M,"
"),makePredByNodeName:E,isEditable:x,isControlSizing:function(t){return t&&r()(t).hasClass("note-control-sizing")},isText:T,isElement:function(t){return t&&1===t.nodeType},isVoid:P,isPara:N,isPurePara:function(t){return N(t)&&!$(t)},isHeading:function(t){return t&&/^H[1-7]/.test(t.nodeName.toUpperCase())},isInline:L,isBlock:g.not(L),isBodyInline:function(t){return L(t)&&!q(t,N)},isBody:B,isParaInline:function(t){return L(t)&&!!q(t,N)},isPre:I,isList:F,isTable:R,isData:A,isCell:H,isBlockquote:j,isBodyContainer:z,isAnchor:O,isDiv:E("DIV"),isLi:$,isBR:E("BR"),isSpan:E("SPAN"),isB:E("B"),isU:E("U"),isS:E("S"),isI:E("I"),isImg:E("IMG"),isTextarea:ft,deepestChildIsEmpty:function(t){do{if(null===t.firstElementChild||""===t.firstElementChild.innerHTML)break}while(t=t.firstElementChild);return W(t)},isEmpty:W,isEmptyAnchor:g.and(O,W),isClosestSibling:function(t,e){return t.nextSibling===e||t.previousSibling===e},withClosestSiblings:function(t,e){e=e||g.ok;var o=[];return t.previousSibling&&e(t.previousSibling)&&o.push(t.previousSibling),o.push(t),t.nextSibling&&e(t.nextSibling)&&o.push(t.nextSibling),o},nodeLength:U,isLeftEdgePoint:Y,isRightEdgePoint:X,isEdgePoint:Q,isLeftEdgeOf:J,isRightEdgeOf:tt,isLeftEdgePointOf:function(t,e){return Y(t)&&J(t.node,e)},isRightEdgePointOf:function(t,e){return X(t)&&tt(t.node,e)},prevPoint:nt,nextPoint:it,nextPointWithEmptyNode:rt,isSamePoint:st,isVisiblePoint:function(t){if(T(t.node)||!ot(t.node)||W(t.node))return!0;var e=t.node.childNodes[t.offset-1],o=t.node.childNodes[t.offset];return!((e&&!P(e)||o&&!P(o))&&!R(o))},prevPointUntil:function(t,e){for(;t;){if(e(t))return t;t=nt(t)}return null},nextPointUntil:function(t,e){for(;t;){if(e(t))return t;t=it(t)}return null},isCharPoint:function(t){if(!T(t.node))return!1;var e=t.node.nodeValue.charAt(t.offset-1);return e&&" "!==e&&e!==S},isSpacePoint:function(t){if(!T(t.node))return!1;var e=t.node.nodeValue.charAt(t.offset-1);return" "===e||e===S},walkPoint:function(t,e,o,n){for(var i=t;i&&i.node&&(o(i),!st(i,e));){i=rt(i,n&&t.node!==i.node&&e.node!==i.node)}},ancestor:q,singleChildAncestor:function(t,e){for(t=t.parentNode;t&&1===U(t);){if(e(t))return t;if(x(t))break;t=t.parentNode}return null},listAncestor:V,lastAncestor:function(t,e){var o=V(t);return C.last(o.filter(e))},listNext:_,listPrev:function(t,e){e=e||g.fail;for(var o=[];t&&!e(t);)o.push(t),t=t.previousSibling;return o},listDescendant:function(t,e){var o=[];return e=e||g.ok,function n(i){t!==i&&e(i)&&o.push(i);for(var r=0,a=i.childNodes.length;r-1)return n;return null},wrap:function(t,e){var o=t.parentNode,n=r()("<"+e+">")[0];return o.insertBefore(n,t),n.appendChild(t),n},insertAfter:G,appendChildNodes:Z,position:et,hasChildren:ot,makeOffsetPath:function(t,e){return V(e,g.eq(t)).map(et).reverse()},fromOffsetPath:function(t,e){for(var o=t,n=0,i=e.length;n\s]*)(.*?)(\s*\/?>)/g,(function(t,e,o){o=o.toUpperCase();var n=/^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(o)&&!!e,i=/^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(o);return t+(n||i?"\n":"")}))).trim()}return o},value:ht,posFromPlaceholder:function(t){var e=r()(t),o=e.offset(),n=e.outerHeight(!0);return{left:o.left,top:o.top+n}},attachEvents:function(t,e){Object.keys(e).forEach((function(o){t.on(o,e[o])}))},detachEvents:function(t,e){Object.keys(e).forEach((function(o){t.off(o,e[o])}))},isCustomStyleTag:function(t){return t&&!T(t)&&C.contains(t.classList,"note-styletag")}};function mt(t){return mt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},mt(t)}function vt(t,e){for(var o=0;o1,i=n&&C.head(o),r=n?C.last(o):C.head(o),a=this.modules[i||"editor"];return!i&&this[r]?this[r].apply(this,e):a&&a[r]&&a.shouldInitialize()?a[r].apply(a,e):void 0}}],e&&vt(t.prototype,e),o&&vt(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function yt(t){return yt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},yt(t)}function kt(t){return kt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},kt(t)}function wt(t,e){for(var o=0;o=0)break;n=a[o]}if(0!==o&&pt.isText(a[o-1])){var s=document.body.createTextRange(),l=null;s.moveToElementText(n||i),s.collapse(!n),l=n?n.nextSibling:i.firstChild;var c=t.duplicate();c.setEndPoint("StartToStart",s);for(var u=c.text.replace(/[\r\n]/g,"").length;u>l.nodeValue.length&&l.nextSibling;)u-=l.nodeValue.length,l=l.nextSibling;l.nodeValue;e&&l.nextSibling&&pt.isText(l.nextSibling)&&u===l.nodeValue.length&&(u-=l.nodeValue.length,l=l.nextSibling),i=l,o=u}return{cont:i,offset:o}}function xt(t){var e=document.body.createTextRange(),o=function t(e,o){var n,i;if(pt.isText(e)){var r=pt.listPrev(e,g.not(pt.isText)),a=C.last(r).previousSibling;n=a||e.parentNode,o+=C.sum(C.tail(r),pt.nodeLength),i=!a}else{if(n=e.childNodes[o]||e,pt.isText(n))return t(n,0);o=0,i=!1}return{node:n,collapseToStart:i,offset:o}}(t.node,t.offset);return e.moveToElementText(o.node),e.collapse(o.collapseToStart),e.moveStart("character",o.offset),e}r().fn.extend({summernote:function(){var t=yt(C.head(arguments)),e="string"===t,o="object"===t,n=r().extend({},r().summernote.options,o?C.head(arguments):{});n.langInfo=r().extend(!0,{},r().summernote.lang["en-US"],r().summernote.lang[n.lang]),n.icons=r().extend(!0,{},r().summernote.options.icons,n.icons),n.tooltip="auto"===n.tooltip?!m.isSupportTouch:n.tooltip,this.each((function(t,e){var o=r()(e);if(!o.data("summernote")){var i=new bt(o,n);o.data("summernote",i),o.data("summernote").triggerEvent("init",i.layoutInfo)}}));var i=this.first();if(i.length){var a=i.data("summernote");if(e)return a.invoke.apply(a,C.from(arguments));n.focus&&a.invoke("editor.focus")}return this}});var Et=function(){function t(e,o,n,i){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.sc=e,this.so=o,this.ec=n,this.eo=i,this.isOnEditable=this.makeIsOn(pt.isEditable),this.isOnList=this.makeIsOn(pt.isList),this.isOnAnchor=this.makeIsOn(pt.isAnchor),this.isOnCell=this.makeIsOn(pt.isCell),this.isOnData=this.makeIsOn(pt.isData)}return e=t,o=[{key:"nativeRange",value:function(){if(m.isW3CRangeSupport){var t=document.createRange();return t.setStart(this.sc,this.so),t.setEnd(this.ec,this.eo),t}var e=xt({node:this.sc,offset:this.so});return e.setEndPoint("EndToEnd",xt({node:this.ec,offset:this.eo})),e}},{key:"getPoints",value:function(){return{sc:this.sc,so:this.so,ec:this.ec,eo:this.eo}}},{key:"getStartPoint",value:function(){return{node:this.sc,offset:this.so}}},{key:"getEndPoint",value:function(){return{node:this.ec,offset:this.eo}}},{key:"select",value:function(){var t=this.nativeRange();if(m.isW3CRangeSupport){var e=document.getSelection();e.rangeCount>0&&e.removeAllRanges(),e.addRange(t)}else t.select();return this}},{key:"scrollIntoView",value:function(t){var e=r()(t).height();return t.scrollTop+e0?o.so-1:0];if(e){var i=pt.listPrev(e,pt.isParaInline).reverse();if((i=i.concat(pt.listNext(e.nextSibling,pt.isParaInline))).length){var r=pt.wrap(C.head(i),"p");pt.appendChildNodes(r,C.tail(i))}}return this.normalize()}},{key:"insertNode",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],o=this;(pt.isText(t)||pt.isInline(t))&&(o=this.wrapBodyInlineWithPara().deleteContents());var n=pt.splitPoint(o.getStartPoint(),pt.isInline(t));return n.rightNode?(n.rightNode.parentNode.insertBefore(t,n.rightNode),pt.isEmpty(n.rightNode)&&(e||pt.isPara(t))&&n.rightNode.parentNode.removeChild(n.rightNode)):n.container.appendChild(t),t}},{key:"pasteHTML",value:function(t){t=((t||"")+"").trim(t);var e=r()("").html(t)[0],o=C.from(e.childNodes),n=this,i=!1;return n.so>=0&&(o=o.reverse(),i=!0),o=o.map((function(t){return n.insertNode(t,!pt.isInline(t))})),i&&(o=o.reverse()),o}},{key:"toString",value:function(){var t=this.nativeRange();return m.isW3CRangeSupport?t.toString():t.text}},{key:"getWordRange",value:function(e){var o=this.getEndPoint();if(!pt.isCharPoint(o))return this;var n=pt.prevPointUntil(o,(function(t){return!pt.isCharPoint(t)}));return e&&(o=pt.nextPointUntil(o,(function(t){return!pt.isCharPoint(t)}))),new t(n.node,n.offset,o.node,o.offset)}},{key:"getWordsRange",value:function(e){var o=this.getEndPoint(),n=function(t){return!pt.isCharPoint(t)&&!pt.isSpacePoint(t)};if(n(o))return this;var i=pt.prevPointUntil(o,n);return e&&(o=pt.nextPointUntil(o,n)),new t(i.node,i.offset,o.node,o.offset)}},{key:"getWordsMatchRange",value:function(e){var o=this.getEndPoint(),n=pt.prevPointUntil(o,(function(n){if(!pt.isCharPoint(n)&&!pt.isSpacePoint(n))return!0;var i=new t(n.node,n.offset,o.node,o.offset),r=e.exec(i.toString());return r&&0===r.index})),i=new t(n.node,n.offset,o.node,o.offset),r=i.toString(),a=e.exec(r);return a&&a[0].length===r.length?i:null}},{key:"bookmark",value:function(t){return{s:{path:pt.makeOffsetPath(t,this.sc),offset:this.so},e:{path:pt.makeOffsetPath(t,this.ec),offset:this.eo}}}},{key:"paraBookmark",value:function(t){return{s:{path:C.tail(pt.makeOffsetPath(C.head(t),this.sc)),offset:this.so},e:{path:C.tail(pt.makeOffsetPath(C.last(t),this.ec)),offset:this.eo}}}},{key:"getClientRects",value:function(){return this.nativeRange().getClientRects()}}],o&&wt(e.prototype,o),n&&wt(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e;var e,o,n}();const Tt={create:function(t,e,o,n){if(4===arguments.length)return new Et(t,e,o,n);if(2===arguments.length)return new Et(t,e,o=t,n=e);var i=this.createFromSelection();if(!i&&1===arguments.length){var r=arguments[0];return pt.isEditable(r)&&(r=r.lastChild),this.createFromBodyElement(r,pt.emptyPara===arguments[0].innerHTML)}return i},createFromBodyElement:function(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return this.createFromNode(t).collapse(e)},createFromSelection:function(){var t,e,o,n;if(m.isW3CRangeSupport){var i=document.getSelection();if(!i||0===i.rangeCount)return null;if(pt.isBody(i.anchorNode))return null;var r=i.getRangeAt(0);t=r.startContainer,e=r.startOffset,o=r.endContainer,n=r.endOffset}else{var a=document.selection.createRange(),s=a.duplicate();s.collapse(!1);var l=a;l.collapse(!0);var c=St(l,!0),u=St(s,!1);pt.isText(c.node)&&pt.isLeftEdgePoint(c)&&pt.isTextNode(u.node)&&pt.isRightEdgePoint(u)&&u.node.nextSibling===c.node&&(c=u),t=c.cont,e=c.offset,o=u.cont,n=u.offset}return new Et(t,e,o,n)},createFromNode:function(t){var e=t,o=0,n=t,i=pt.nodeLength(n);return pt.isVoid(e)&&(o=pt.listPrev(e).length-1,e=e.parentNode),pt.isBR(n)?(i=pt.listPrev(n).length-1,n=n.parentNode):pt.isVoid(n)&&(i=pt.listPrev(n).length,n=n.parentNode),this.create(e,o,n,i)},createFromNodeBefore:function(t){return this.createFromNode(t).collapse(!0)},createFromNodeAfter:function(t){return this.createFromNode(t).collapse()},createFromBookmark:function(t,e){var o=pt.fromOffsetPath(t,e.s.path),n=e.s.offset,i=pt.fromOffsetPath(t,e.e.path),r=e.e.offset;return new Et(o,n,i,r)},createFromParaBookmark:function(t,e){var o=t.s.offset,n=t.e.offset,i=pt.fromOffsetPath(C.head(e),t.s.path),r=pt.fromOffsetPath(C.last(e),t.e.path);return new Et(i,o,r,n)}};var Pt={BACKSPACE:8,TAB:9,ENTER:13,ESCAPE:27,SPACE:32,DELETE:46,LEFT:37,UP:38,RIGHT:39,DOWN:40,NUM0:48,NUM1:49,NUM2:50,NUM3:51,NUM4:52,NUM5:53,NUM6:54,NUM7:55,NUM8:56,B:66,E:69,I:73,J:74,K:75,L:76,R:82,S:83,U:85,V:86,Y:89,Z:90,SLASH:191,LEFTBRACKET:219,BACKSLASH:220,RIGHTBRACKET:221,HOME:36,END:35,PAGEUP:33,PAGEDOWN:34};const Nt={isEdit:function(t){return C.contains([Pt.BACKSPACE,Pt.TAB,Pt.ENTER,Pt.SPACE,Pt.DELETE],t)},isRemove:function(t){return C.contains([Pt.BACKSPACE,Pt.DELETE],t)},isMove:function(t){return C.contains([Pt.LEFT,Pt.UP,Pt.RIGHT,Pt.DOWN],t)},isNavigation:function(t){return C.contains([Pt.HOME,Pt.END,Pt.PAGEUP,Pt.PAGEDOWN],t)},nameFromCode:g.invertObject(Pt),code:Pt};function It(t){return It="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},It(t)}function $t(t,e){for(var o=0;o0&&(this.stackOffset--,this.applySnapshot(this.stack[this.stackOffset]))}},{key:"redo",value:function(){this.stack.length-1>this.stackOffset&&(this.stackOffset++,this.applySnapshot(this.stack[this.stackOffset]))}},{key:"recordUndo",value:function(){this.stackOffset++,this.stack.length>this.stackOffset&&(this.stack=this.stack.slice(0,this.stackOffset)),this.stack.push(this.makeSnapshot()),this.stack.length>this.context.options.historyLimit&&(this.stack.shift(),this.stackOffset-=1)}}])&&$t(t.prototype,e),o&&$t(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Lt(t){return Lt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Lt(t)}function Ft(t,e){for(var o=0;o-1;o["list-style"]=n?"unordered":"ordered"}else o["list-style"]="none";var i=pt.ancestor(t.sc,pt.isPara);if(i&&i.style["line-height"])o["line-height"]=i.style.lineHeight;else{var a=parseInt(o["line-height"],10)/parseInt(o["font-size"],10);o["line-height"]=a.toFixed(1)}return o.anchor=t.isOnAnchor()&&pt.ancestor(t.sc,pt.isAnchor),o.ancestors=pt.listAncestor(t.sc,pt.isEditable),o.range=t,o}}],e&&Ft(t.prototype,e),o&&Ft(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function jt(t){return jt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},jt(t)}function zt(t,e){for(var o=0;o25?e-25:""}))}))})),o.select()}},{key:"toggleList",value:function(t,e){var o=this,n=Tt.create(e).wrapBodyInlineWithPara(),i=n.nodes(pt.isPara,{includeAncestor:!0}),a=n.paraBookmark(i),s=C.clusterBy(i,g.peq2("parentNode"));if(C.find(i,pt.isPurePara)){var l=[];r().each(s,(function(e,n){l=l.concat(o.wrapList(n,t))})),i=l}else{var c=n.nodes(pt.isList,{includeAncestor:!0}).filter((function(e){return!r().nodeName(e,t)}));c.length?r().each(c,(function(e,o){pt.replace(o,t)})):i=this.releaseList(s,!0)}Tt.createFromParaBookmark(a,i).select()}},{key:"wrapList",value:function(t,e){var o=C.head(t),n=C.last(t),i=pt.isList(o.previousSibling)&&o.previousSibling,r=pt.isList(n.nextSibling)&&n.nextSibling,a=i||pt.insertAfter(pt.create(e||"UL"),n);return t=t.map((function(t){return pt.isPurePara(t)?pt.replace(t,"LI"):t})),pt.appendChildNodes(a,t,!0),r&&(pt.appendChildNodes(a,C.from(r.childNodes),!0),pt.remove(r)),t}},{key:"releaseList",value:function(t,e){var o=this,n=[];return r().each(t,(function(t,i){var a=C.head(i),s=C.last(i),l=e?pt.lastAncestor(a,pt.isList):a.parentNode,c=l.parentNode;if("LI"===l.parentNode.nodeName)i.map((function(t){var e=o.findNextSiblings(t);c.nextSibling?c.parentNode.insertBefore(t,c.nextSibling):c.parentNode.appendChild(t),e.length&&(o.wrapList(e,l.nodeName),t.appendChild(e[0].parentNode))})),0===l.children.length&&c.removeChild(l),0===c.childNodes.length&&c.parentNode.removeChild(c);else{var u=l.childNodes.length>1?pt.splitTree(l,{node:s.parentNode,offset:pt.position(s)+1},{isSkipPaddingBlankHTML:!0}):null,d=pt.splitTree(l,{node:a.parentNode,offset:pt.position(a)},{isSkipPaddingBlankHTML:!0});i=e?pt.listDescendant(d,pt.isLi):C.from(d.childNodes).filter(pt.isLi),!e&&pt.isList(l.parentNode)||(i=i.map((function(t){return pt.replace(t,"P")}))),r().each(C.from(i).reverse(),(function(t,e){pt.insertAfter(e,l)}));var f=C.compact([l,d,u]);r().each(f,(function(t,e){var o=[e].concat(pt.listDescendant(e,pt.isList));r().each(o.reverse(),(function(t,e){pt.nodeLength(e)||pt.remove(e,!0)}))}))}n=n.concat(i)})),n}},{key:"appendToPrevious",value:function(t){return t.previousSibling?pt.appendChildNodes(t.previousSibling,[t]):this.wrapList([t],"LI")}},{key:"findList",value:function(t){return t?C.find(t.children,(function(t){return["OL","UL"].indexOf(t.nodeName)>-1})):null}},{key:"findNextSiblings",value:function(t){for(var e=[];t.nextSibling;)e.push(t.nextSibling),t=t.nextSibling;return e}}],e&&zt(t.prototype,e),o&&zt(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Mt(t){return Mt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Mt(t)}function Ut(t,e){for(var o=0;o1,i=e.rowSpan>1,a=t.rowIndex===r.rowPos&&e.cellIndex===r.colPos;l(t.rowIndex,o,t,e,i,n,!1);var s=e.attributes.rowSpan?parseInt(e.attributes.rowSpan.value,10):0;if(s>1)for(var c=1;c1)for(var p=1;p=o.cellIndex&&o.cellIndex<=e&&!n&&r.colPos++}function h(e){switch(o){case t.where.Column:if(e.isColSpan)return t.resultAction.SubtractSpanCount;break;case t.where.Row:if(!e.isVirtual&&e.isRowSpan)return t.resultAction.AddCell;if(e.isRowSpan)return t.resultAction.SubtractSpanCount}return t.resultAction.RemoveCell}function p(e){switch(o){case t.where.Column:if(e.isColSpan)return t.resultAction.SumSpanCount;if(e.isRowSpan&&e.isVirtual)return t.resultAction.Ignore;break;case t.where.Row:if(e.isRowSpan)return t.resultAction.SumSpanCount;if(e.isColSpan&&e.isVirtual)return t.resultAction.Ignore}return t.resultAction.AddCell}this.getActionList=function(){for(var e=o===t.where.Row?r.rowPos:-1,i=o===t.where.Column?r.colPos:-1,l=0,u=!0;u;){var d=e>=0?e:l,f=i>=0?i:l,m=a[d];if(!m)return u=!1,s;var v=m[f];if(!v)return u=!1,s;var g=t.resultAction.Ignore;switch(n){case t.requestAction.Add:g=p(v);break;case t.requestAction.Delete:g=h(v)}s.push(c(v,g,d,f)),l++}return s},e&&e.tagName&&("td"===e.tagName.toLowerCase()||"th"===e.tagName.toLowerCase())&&(r.colPos=e.cellIndex,e.parentElement&&e.parentElement.tagName&&"tr"===e.parentElement.tagName.toLowerCase()&&(r.rowPos=e.parentElement.rowIndex)),function(){for(var t=i.rows,e=0;e"),s=new Gt(o,Gt.where.Row,Gt.requestAction.Add,r()(n).closest("table")[0]).getActionList(),l=0;l"+pt.blank+"");break;case Gt.resultAction.SumSpanCount:if("top"===e&&(c.baseCell.parent?c.baseCell.closest("tr").rowIndex:0)<=n[0].rowIndex){var d=r()("").append(r()("| "+pt.blank+" | ").removeAttr("rowspan")).html();a.append(d);break}var f=parseInt(c.baseCell.rowSpan,10);f++,c.baseCell.setAttribute("rowSpan",f)}}if("top"===e)n.before(a);else{if(o.rowSpan>1){var h=n[0].rowIndex+(o.rowSpan-2);return void r()(r()(n).parent().find("tr")[h]).after(r()(a))}n.after(a)}}},{key:"addCol",value:function(t,e){var o=pt.ancestor(t.commonAncestor(),pt.isCell),n=r()(o).closest("tr");r()(n).siblings().push(n);for(var i=new Gt(o,Gt.where.Column,Gt.requestAction.Add,r()(n).closest("table")[0]).getActionList(),a=0;a"+pt.blank+""):r()(s.baseCell).before(""+pt.blank+" | ");break;case Gt.resultAction.SumSpanCount:if("right"===e){var c=parseInt(s.baseCell.colSpan,10);c++,s.baseCell.setAttribute("colSpan",c)}else r()(s.baseCell).before(""+pt.blank+" | ")}}}},{key:"recoverAttributes",value:function(t){var e="";if(!t)return e;for(var o=t.attributes||[],n=0;n1,d=u?parseInt(l.rowSpan,10):0;switch(a[s].action){case Gt.resultAction.Ignore:continue;case Gt.resultAction.AddCell:var f=o.next("tr")[0];if(!f)continue;var h=o[0].cells[n];u&&(d>2?(d--,f.insertBefore(h,f.cells[n]),f.cells[n].setAttribute("rowSpan",d),f.cells[n].innerHTML=""):2===d&&(f.insertBefore(h,f.cells[n]),f.cells[n].removeAttribute("rowSpan"),f.cells[n].innerHTML=""));continue;case Gt.resultAction.SubtractSpanCount:u&&(d>2?(d--,l.setAttribute("rowSpan",d),c.rowIndex!==i&&l.cellIndex===n&&(l.innerHTML="")):2===d&&(l.removeAttribute("rowSpan"),c.rowIndex!==i&&l.cellIndex===n&&(l.innerHTML="")));continue;case Gt.resultAction.RemoveCell:continue}}o.remove()}},{key:"deleteCol",value:function(t){for(var e=pt.ancestor(t.commonAncestor(),pt.isCell),o=r()(e).closest("tr"),n=o.children("td, th").index(r()(e)),i=new Gt(e,Gt.where.Column,Gt.requestAction.Delete,r()(o).closest("table")[0]).getActionList(),a=0;a1){var l=s.colSpan?parseInt(s.colSpan,10):0;l>2?(l--,s.setAttribute("colSpan",l),s.cellIndex===n&&(s.innerHTML="")):2===l&&(s.removeAttribute("colSpan"),s.cellIndex===n&&(s.innerHTML=""))}continue;case Gt.resultAction.RemoveCell:pt.remove(i[a].baseCell,!0);continue}}},{key:"createTable",value:function(t,e,o){for(var n,i=[],a=0;a"+pt.blank+"");n=i.join("");for(var s,l=[],c=0;c"+n+"");s=l.join("");var u=r()("");return o&&o.tableClassName&&u.addClass(o.tableClassName),u[0]}},{key:"deleteTable",value:function(t){var e=pt.ancestor(t.commonAncestor(),pt.isCell);r()(e).closest("table").remove()}}],e&&Vt(t.prototype,e),o&&Vt(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Yt(t){return Yt="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Yt(t)}function Xt(t,e){for(var o=0;o0&&o.isLimited(u))){var d=c.toString()!==i;"string"==typeof n&&(n=n.trim()),n=o.options.onCreateLink?o.options.onCreateLink(n):o.checkLinkUrl(n);var f=[];if(d){var h=(c=c.deleteContents()).insertNode(r()("").text(i)[0]);f.push(h)}else f=o.style.styleNodes(c,{nodeName:"A",expandClosestSibling:!0,onlyPartialContains:!0});r().each(f,(function(t,o){r()(o).attr("href",n),a?(r()(o).attr("target","_blank"),s&&e.push("noreferrer"),l&&e.push("noopener"),e.length&&r()(o).attr("rel",e.join(" "))):r()(o).removeAttr("target")})),o.setLastRange(o.createRangeFromList(f).select())}})),this.color=this.wrapCommand((function(t){var e=t.foreColor,o=t.backColor;e&&document.execCommand("foreColor",!1,e),o&&document.execCommand("backColor",!1,o)})),this.foreColor=this.wrapCommand((function(t){document.execCommand("foreColor",!1,t)})),this.insertTable=this.wrapCommand((function(t){var e=t.split("x");o.getLastRange().deleteContents().insertNode(o.table.createTable(e[0],e[1],o.options))})),this.removeMedia=this.wrapCommand((function(){var t=r()(o.restoreTarget()).parent();t.closest("figure").length?t.closest("figure").remove():t=r()(o.restoreTarget()).detach(),o.setLastRange(Tt.createFromSelection(t).select()),o.context.triggerEvent("media.delete",t,o.$editable)})),this.floatMe=this.wrapCommand((function(t){var e=r()(o.restoreTarget());e.toggleClass("note-float-left","left"===t),e.toggleClass("note-float-right","right"===t),e.css("float","none"===t?"":t)})),this.resize=this.wrapCommand((function(t){var e=r()(o.restoreTarget());0===(t=parseFloat(t))?e.css("width",""):e.css({width:100*t+"%",height:""})}))},e=[{key:"initialize",value:function(){var t=this;this.$editable.on("keydown",(function(e){if(e.keyCode===Nt.code.ENTER&&t.context.triggerEvent("enter",e),t.context.triggerEvent("keydown",e),t.snapshot=t.history.makeSnapshot(),t.hasKeyShortCut=!1,e.isDefaultPrevented()||(t.options.shortcuts?t.hasKeyShortCut=t.handleKeyMap(e):t.preventDefaultEditableShortCuts(e)),t.isLimited(1,e)){var o=t.getLastRange();if(o.eo-o.so==0)return!1}t.setLastRange(),t.options.recordEveryKeystroke&&!1===t.hasKeyShortCut&&t.history.recordUndo()})).on("keyup",(function(e){t.setLastRange(),t.context.triggerEvent("keyup",e)})).on("focus",(function(e){t.setLastRange(),t.context.triggerEvent("focus",e)})).on("blur",(function(e){t.context.triggerEvent("blur",e)})).on("mousedown",(function(e){t.context.triggerEvent("mousedown",e)})).on("mouseup",(function(e){t.setLastRange(),t.history.recordUndo(),t.context.triggerEvent("mouseup",e)})).on("scroll",(function(e){t.context.triggerEvent("scroll",e)})).on("paste",(function(e){t.setLastRange(),t.context.triggerEvent("paste",e)})).on("copy",(function(e){t.context.triggerEvent("copy",e)})).on("input",(function(){t.isLimited(0)&&t.snapshot&&t.history.applySnapshot(t.snapshot)})),this.$editable.attr("spellcheck",this.options.spellCheck),this.$editable.attr("autocorrect",this.options.spellCheck),this.options.disableGrammar&&this.$editable.attr("data-gramm",!1),this.$editable.html(pt.html(this.$note)||pt.emptyPara),this.$editable.on(m.inputEventName,g.debounce((function(){t.context.triggerEvent("change",t.$editable.html(),t.$editable)}),10)),this.$editable.on("focusin",(function(e){t.context.triggerEvent("focusin",e)})).on("focusout",(function(e){t.context.triggerEvent("focusout",e)})),this.options.airMode?this.options.overrideContextMenu&&this.$editor.on("contextmenu",(function(e){return t.context.triggerEvent("contextmenu",e),!1})):(this.options.width&&this.$editor.outerWidth(this.options.width),this.options.height&&this.$editable.outerHeight(this.options.height),this.options.maxHeight&&this.$editable.css("max-height",this.options.maxHeight),this.options.minHeight&&this.$editable.css("min-height",this.options.minHeight)),this.history.recordUndo(),this.setLastRange()}},{key:"destroy",value:function(){this.$editable.off()}},{key:"handleKeyMap",value:function(t){var e=this.options.keyMap[m.isMac?"mac":"pc"],o=[];t.metaKey&&o.push("CMD"),t.ctrlKey&&!t.altKey&&o.push("CTRL"),t.shiftKey&&o.push("SHIFT");var n=Nt.nameFromCode[t.keyCode];n&&o.push(n);var i=e[o.join("+")];if("TAB"!==n||this.options.tabDisable)if(i){if(!1!==this.context.invoke(i))return t.preventDefault(),!0}else Nt.isEdit(t.keyCode)&&(Nt.isRemove(t.keyCode)&&this.context.invoke("removed"),this.afterCommand());else this.afterCommand();return!1}},{key:"preventDefaultEditableShortCuts",value:function(t){(t.ctrlKey||t.metaKey)&&C.contains([66,73,85],t.keyCode)&&t.preventDefault()}},{key:"isLimited",value:function(t,e){return t=t||0,(void 0===e||!(Nt.isMove(e.keyCode)||Nt.isNavigation(e.keyCode)||e.ctrlKey||e.metaKey||C.contains([Nt.code.BACKSPACE,Nt.code.DELETE],e.keyCode)))&&this.options.maxTextLength>0&&this.$editable.text().length+t>this.options.maxTextLength}},{key:"checkLinkUrl",value:function(t){return Jt.test(t)?"mailto://"+t:te.test(t)?"tel://"+t:ee.test(t)?t:"http://"+t}},{key:"createRange",value:function(){return this.focus(),this.setLastRange(),this.getLastRange()}},{key:"createRangeFromList",value:function(t){var e=Tt.createFromNodeBefore(C.head(t)).getStartPoint(),o=Tt.createFromNodeAfter(C.last(t)).getEndPoint();return Tt.create(e.node,e.offset,o.node,o.offset)}},{key:"setLastRange",value:function(t){t?this.lastRange=t:(this.lastRange=Tt.create(this.editable),0===r()(this.lastRange.sc).closest(".note-editable").length&&(this.lastRange=Tt.createFromBodyElement(this.editable)))}},{key:"getLastRange",value:function(){return this.lastRange||this.setLastRange(),this.lastRange}},{key:"saveRange",value:function(t){t&&this.getLastRange().collapse().select()}},{key:"restoreRange",value:function(){this.lastRange&&(this.lastRange.select(),this.focus())}},{key:"saveTarget",value:function(t){this.$editable.data("target",t)}},{key:"clearTarget",value:function(){this.$editable.removeData("target")}},{key:"restoreTarget",value:function(){return this.$editable.data("target")}},{key:"currentStyle",value:function(){var t=Tt.create();return t&&(t=t.normalize()),t?this.style.current(t):this.style.fromNode(this.$editable)}},{key:"styleFromNode",value:function(t){return this.style.fromNode(t)}},{key:"undo",value:function(){this.context.triggerEvent("before.command",this.$editable.html()),this.history.undo(),this.context.triggerEvent("change",this.$editable.html(),this.$editable)}},{key:"commit",value:function(){this.context.triggerEvent("before.command",this.$editable.html()),this.history.commit(),this.context.triggerEvent("change",this.$editable.html(),this.$editable)}},{key:"redo",value:function(){this.context.triggerEvent("before.command",this.$editable.html()),this.history.redo(),this.context.triggerEvent("change",this.$editable.html(),this.$editable)}},{key:"beforeCommand",value:function(){this.context.triggerEvent("before.command",this.$editable.html()),document.execCommand("styleWithCSS",!1,this.options.styleWithCSS),this.focus()}},{key:"afterCommand",value:function(t){this.normalizeContent(),this.history.recordUndo(),t||this.context.triggerEvent("change",this.$editable.html(),this.$editable)}},{key:"tab",value:function(){var t=this.getLastRange();if(t.isCollapsed()&&t.isOnCell())this.table.tab(t);else{if(0===this.options.tabSize)return!1;this.isLimited(this.options.tabSize)||(this.beforeCommand(),this.typing.insertTab(t,this.options.tabSize),this.afterCommand())}}},{key:"untab",value:function(){var t=this.getLastRange();if(t.isCollapsed()&&t.isOnCell())this.table.tab(t,!0);else if(0===this.options.tabSize)return!1}},{key:"wrapCommand",value:function(t){return function(){this.beforeCommand(),t.apply(this,arguments),this.afterCommand()}}},{key:"removed",value:function(t,e,o){(t=Tt.create()).isCollapsed()&&t.isOnCell()&&(o=(e=t.ec).tagName)&&1===e.childElementCount&&"BR"===e.childNodes[0].tagName&&("P"===o?e.remove():["TH","TD"].indexOf(o)>=0&&e.firstChild.remove())}},{key:"insertImage",value:function(t,e){var o,n=this;return(o=t,r().Deferred((function(t){var e=r()("
");e.one("load",(function(){e.off("error abort"),t.resolve(e)})).one("error abort",(function(){e.off("load").detach(),t.reject(e)})).css({display:"none"}).appendTo(document.body).attr("src",o)})).promise()).then((function(t){n.beforeCommand(),"function"==typeof e?e(t):("string"==typeof e&&t.attr("data-filename",e),t.css("width",Math.min(n.$editable.width(),t.width()))),t.show(),n.getLastRange().insertNode(t[0]),n.setLastRange(Tt.createFromNodeAfter(t[0]).select()),n.afterCommand()})).fail((function(t){n.context.triggerEvent("image.upload.error",t)}))}},{key:"insertImagesAsDataURL",value:function(t){var e=this;r().each(t,(function(t,o){var n=o.name;e.options.maximumImageFileSize&&e.options.maximumImageFileSize":t),e&&e.length&&(e[0].tagName.toUpperCase()!==t.toUpperCase()&&(e=e.find(t)),e&&e.length)){var o=this.createRange(),n=r()([o.sc,o.ec]).closest(t);n.removeClass();var i=e[0].className||"";i&&n.addClass(i)}}},{key:"formatPara",value:function(){this.formatBlock("P")}},{key:"fontStyling",value:function(t,e){var o=this.getLastRange();if(""!==o){var n=this.style.styleNodes(o);if(this.$editor.find(".note-status-output").html(""),r()(n).css(t,e),o.isCollapsed()){var i=C.head(n);i&&!pt.nodeLength(i)&&(i.innerHTML=pt.ZERO_WIDTH_NBSP_CHAR,Tt.createFromNode(i.firstChild).select(),this.setLastRange(),this.$editable.data("bogus",i))}else o.select()}else{var a=r().now();this.$editor.find(".note-status-output").html(''+this.lang.output.noSelection+"
"),setTimeout((function(){r()("#note-status-output-"+a).remove()}),5e3)}}},{key:"unlink",value:function(){var t=this.getLastRange();if(t.isOnAnchor()){var e=pt.ancestor(t.sc,pt.isAnchor);(t=Tt.createFromNode(e)).select(),this.setLastRange(),this.beforeCommand(),document.execCommand("unlink"),this.afterCommand()}}},{key:"getLinkInfo",value:function(){this.hasFocus()||this.focus();var t=this.getLastRange().expand(pt.isAnchor),e=r()(C.head(t.nodes(pt.isAnchor))),o={range:t,text:t.toString(),url:e.length?e.attr("href"):""};return e.length&&(o.isNewWindow="_blank"===e.attr("target")),o}},{key:"addRow",value:function(t){var e=this.getLastRange(this.$editable);e.isCollapsed()&&e.isOnCell()&&(this.beforeCommand(),this.table.addRow(e,t),this.afterCommand())}},{key:"addCol",value:function(t){var e=this.getLastRange(this.$editable);e.isCollapsed()&&e.isOnCell()&&(this.beforeCommand(),this.table.addCol(e,t),this.afterCommand())}},{key:"deleteRow",value:function(){var t=this.getLastRange(this.$editable);t.isCollapsed()&&t.isOnCell()&&(this.beforeCommand(),this.table.deleteRow(t),this.afterCommand())}},{key:"deleteCol",value:function(){var t=this.getLastRange(this.$editable);t.isCollapsed()&&t.isOnCell()&&(this.beforeCommand(),this.table.deleteCol(t),this.afterCommand())}},{key:"deleteTable",value:function(){var t=this.getLastRange(this.$editable);t.isCollapsed()&&t.isOnCell()&&(this.beforeCommand(),this.table.deleteTable(t),this.afterCommand())}},{key:"resizeTo",value:function(t,e,o){var n;if(o){var i=t.y/t.x,r=e.data("ratio");n={width:r>i?t.x:t.y/r,height:r>i?t.x*r:t.y}}else n={width:t.x,height:t.y};e.css(n)}},{key:"hasFocus",value:function(){return this.$editable.is(":focus")}},{key:"focus",value:function(){this.hasFocus()||this.$editable.trigger("focus")}},{key:"isEmpty",value:function(){return pt.isEmpty(this.$editable[0])||pt.emptyPara===this.$editable.html()}},{key:"empty",value:function(){this.context.invoke("code",pt.emptyPara)}},{key:"normalizeContent",value:function(){this.$editable[0].normalize()}}],e&&Xt(t.prototype,e),o&&Xt(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function ne(t){return ne="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ne(t)}function ie(t,e){for(var o=0;o0&&this.options.allowClipboardImagePasting&&(this.context.invoke("editor.insertImagesOrCallback",n),t.preventDefault()),i.length>0&&this.context.invoke("editor.isLimited",i.length)&&t.preventDefault()}else if(window.clipboardData){var r=window.clipboardData.getData("text");this.context.invoke("editor.isLimited",r.length)&&t.preventDefault()}setTimeout((function(){e.context.invoke("editor.afterCommand")}),10)}}}])&&ie(t.prototype,e),o&&ie(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function se(t){return se="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},se(t)}function le(t,e){for(var o=0;o','',""].join("")).prependTo(this.$editor)},e=[{key:"initialize",value:function(){this.options.disableDragAndDrop?(this.documentEventHandlers.onDrop=function(t){t.preventDefault()},this.$eventListener=this.$dropzone,this.$eventListener.on("drop",this.documentEventHandlers.onDrop)):this.attachDragAndDropEvent()}},{key:"attachDragAndDropEvent",value:function(){var t=this,e=r()(),o=this.$dropzone.find(".note-dropzone-message");this.documentEventHandlers.onDragenter=function(n){var i=t.context.invoke("codeview.isActivated"),r=t.$editor.width()>0&&t.$editor.height()>0;i||e.length||!r||(t.$editor.addClass("dragover"),t.$dropzone.width(t.$editor.width()),t.$dropzone.height(t.$editor.height()),o.text(t.lang.image.dragImageHere)),e=e.add(n.target)},this.documentEventHandlers.onDragleave=function(o){(e=e.not(o.target)).length&&"BODY"!==o.target.nodeName||(e=r()(),t.$editor.removeClass("dragover"))},this.documentEventHandlers.onDrop=function(){e=r()(),t.$editor.removeClass("dragover")},this.$eventListener.on("dragenter",this.documentEventHandlers.onDragenter).on("dragleave",this.documentEventHandlers.onDragleave).on("drop",this.documentEventHandlers.onDrop),this.$dropzone.on("dragenter",(function(){t.$dropzone.addClass("hover"),o.text(t.lang.image.dropImage)})).on("dragleave",(function(){t.$dropzone.removeClass("hover"),o.text(t.lang.image.dragImageHere)})),this.$dropzone.on("drop",(function(e){var o=e.originalEvent.dataTransfer;e.preventDefault(),o&&o.files&&o.files.length?(t.$editable.trigger("focus"),t.context.invoke("editor.insertImagesOrCallback",o.files)):r().each(o.types,(function(e,n){if(!(n.toLowerCase().indexOf("_moz_")>-1)){var i=o.getData(n);n.toLowerCase().indexOf("text")>-1?t.context.invoke("editor.pasteHTML",i):r()(i).each((function(e,o){t.context.invoke("editor.insertNode",o)}))}}))})).on("dragover",!1)}},{key:"destroy",value:function(){var t=this;Object.keys(this.documentEventHandlers).forEach((function(e){t.$eventListener.off(e.slice(2).toLowerCase(),t.documentEventHandlers[e])})),this.documentEventHandlers={}}}],e&&le(t.prototype,e),o&&le(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function de(t){return de="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},de(t)}function fe(t,e){var o="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!o){if(Array.isArray(t)||(o=function(t,e){if(t){if("string"==typeof t)return he(t,e);var o={}.toString.call(t).slice(8,-1);return"Object"===o&&t.constructor&&(o=t.constructor.name),"Map"===o||"Set"===o?Array.from(t):"Arguments"===o||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(o)?he(t,e):void 0}}(t))||e&&t&&"number"==typeof t.length){o&&(t=o);var n=0,i=function(){};return{s:i,n:function(){return n>=t.length?{done:!0}:{done:!1,value:t[n++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var r,a=!0,s=!1;return{s:function(){o=o.call(t)},n:function(){var t=o.next();return a=t.done,t},e:function(t){s=!0,r=t},f:function(){try{a||null==o.return||o.return()}finally{if(s)throw r}}}}function he(t,e){(null==e||e>t.length)&&(e=t.length);for(var o=0,n=Array(e);o.*?(?:<\/iframe>)?)/gi,(function(t){if(/<.+src(?==?('|"|\s)?)[\s\S]+src(?=('|"|\s)?)[^>]*?>/i.test(t))return"";var o,n=fe(e);try{for(n.s();!(o=n.n()).done;){var i=o.value;if(new RegExp('src="(https?:)?//'+i.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")+'/(.+)"').test(t))return t}}catch(t){n.e(t)}finally{n.f()}return""}))}return t}},{key:"activate",value:function(){var t=this,e=this.CodeMirrorConstructor;if(this.$codable.val(pt.html(this.$editable,this.options.prettifyHtml)),this.$codable.height(this.$editable.height()),this.context.invoke("toolbar.updateCodeview",!0),this.context.invoke("airPopover.updateCodeview",!0),this.$editor.addClass("codeview"),this.$codable.trigger("focus"),e){var o=e.fromTextArea(this.$codable[0],this.options.codemirror);if(this.options.codemirror.tern){var n=new e.TernServer(this.options.codemirror.tern);o.ternServer=n,o.on("cursorActivity",(function(t){n.updateArgHints(t)}))}o.on("blur",(function(e){t.context.triggerEvent("blur.codeview",o.getValue(),e)})),o.on("change",(function(){t.context.triggerEvent("change.codeview",o.getValue(),o)})),o.setSize(null,this.$editable.outerHeight()),this.$codable.data("cmEditor",o)}else this.$codable.on("blur",(function(e){t.context.triggerEvent("blur.codeview",t.$codable.val(),e)})),this.$codable.on("input",(function(){t.context.triggerEvent("change.codeview",t.$codable.val(),t.$codable)}))}},{key:"deactivate",value:function(){if(this.CodeMirrorConstructor){var t=this.$codable.data("cmEditor");this.$codable.val(t.getValue()),t.toTextArea()}var e=this.purify(pt.value(this.$codable,this.options.prettifyHtml)||pt.emptyPara),o=this.$editable.html()!==e;this.$editable.html(e),this.$editable.height(this.options.height?this.$codable.height():"auto"),this.$editor.removeClass("codeview"),o&&this.context.triggerEvent("change",this.$editable.html(),this.$editable),this.$editable.trigger("focus"),this.context.invoke("toolbar.updateCodeview",!1),this.context.invoke("airPopover.updateCodeview",!1)}},{key:"destroy",value:function(){this.isActivated()&&this.deactivate()}}],e&&pe(t.prototype,e),o&&pe(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function ge(t){return ge="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ge(t)}function be(t,e){for(var o=0;o0?Math.max(r,t.options.minheight):r,r=t.options.maxHeight>0?Math.min(r,t.options.maxHeight):r,a=t.options.minheight>0?Math.max(a,t.options.minheight):a,a=t.options.maxHeight>0?Math.min(a,t.options.maxHeight):a,t.$editable.height(r),t.$codable.height(a)};t.$document.on("mousemove touchmove",i).one("mouseup touchend",(function(){t.$document.off("mousemove touchmove",i)}))}))}},{key:"destroy",value:function(){this.$statusbar.off(),this.$statusbar.addClass("locked")}}])&&be(t.prototype,e),o&&be(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function we(t){return we="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},we(t)}function Ce(t,e){for(var o=0;o','','
','
','
','
','
',this.options.disableResizeImage?"":'
',"
",""].join("")).prependTo(this.$editingArea),this.$handle.on("mousedown",(function(e){if(pt.isControlSizing(e.target)){e.preventDefault(),e.stopPropagation();var o=t.$handle.find(".note-control-selection").data("target"),n=o.offset(),i=t.$document.scrollTop(),r=function(e){t.context.invoke("editor.resizeTo",{x:e.clientX-n.left,y:e.clientY-(n.top-i)},o,!e.shiftKey),t.update(o[0],e)};t.$document.on("mousemove",r).one("mouseup",(function(e){e.preventDefault(),t.$document.off("mousemove",r),t.context.invoke("editor.afterCommand")})),o.data("ratio")||o.data("ratio",o.height()/o.width())}})),this.$handle.on("wheel",(function(e){e.preventDefault(),t.update()}))}},{key:"destroy",value:function(){this.$handle.remove()}},{key:"update",value:function(t,e){if(this.context.isDisabled())return!1;var o=pt.isImg(t),n=this.$handle.find(".note-control-selection");if(this.context.invoke("imagePopover.update",t,e),o){var i=r()(t),a=this.$editingArea[0].getBoundingClientRect(),s=t.getBoundingClientRect();n.css({display:"block",left:s.left-a.left,top:s.top-a.top,width:s.width,height:s.height}).data("target",i);var l=new Image;l.src=i.attr("src");var c=s.width+"x"+s.height+" ("+this.lang.image.original+": "+l.width+"x"+l.height+")";n.find(".note-control-selection-info").text(c),this.context.invoke("editor.saveTarget",t)}else this.hide();return o}},{key:"hide",value:function(){this.context.invoke("editor.clearTarget"),this.$handle.children().hide()}}],e&&Te(t.prototype,e),o&&Te(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Ie(t){return Ie="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ie(t)}function $e(t,e){for(var o=0;o").html(n).attr("href",o)[0];this.context.options.linkTargetBlank&&r()(i).attr("target","_blank"),this.lastWordRange.insertNode(i),this.lastWordRange=null,this.context.invoke("editor.focus"),this.context.triggerEvent("change",this.$editable.html(),this.$editable)}}}},{key:"handleKeydown",value:function(t){if(C.contains([Nt.code.ENTER,Nt.code.SPACE],t.keyCode)){var e=this.context.invoke("editor.createRange").getWordRange();this.lastWordRange=e}}},{key:"handleKeyup",value:function(t){(Nt.code.SPACE===t.keyCode||Nt.code.ENTER===t.keyCode&&!t.shiftKey)&&this.replace()}}])&&$e(t.prototype,e),o&&$e(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Fe(t){return Fe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Fe(t)}function De(t,e){for(var o=0;o'),this.$placeholder.on("click",(function(){t.context.invoke("focus")})).html(this.options.placeholder).prependTo(this.$editingArea),this.update()}},{key:"destroy",value:function(){this.$placeholder.remove()}},{key:"update",value:function(){var t=!this.context.invoke("codeview.isActivated")&&this.context.invoke("editor.isEmpty");this.$placeholder.toggle(t)}}])&&We(t.prototype,e),o&&We(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function Ve(t){return Ve="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Ve(t)}function _e(t,e){for(var o=0;o',''+this.lang.color.background+"
","",'","
",'\x3c!-- back colors --\x3e
',"",'",'',"
",'',""].join(""):"")+(n?['','
'+this.lang.color.foreground+"
","
",'","
",'
\x3c!-- fore colors --\x3e
',"
",'",'',"
",'
',"
"].join(""):""),callback:function(t){t.find(".note-holder").each((function(t,e){var o=r()(e);o.append(i.ui.palette({colors:i.options.colors,colorsName:i.options.colorsName,eventName:o.data("event"),container:i.options.container,tooltip:i.options.tooltip}).render())}));var e=[["#FFFFFF","#FFFFFF","#FFFFFF","#FFFFFF","#FFFFFF","#FFFFFF","#FFFFFF","#FFFFFF"]];t.find(".note-holder-custom").each((function(t,o){var n=r()(o);n.append(i.ui.palette({colors:e,colorsName:e,eventName:n.data("event"),container:i.options.container,tooltip:i.options.tooltip}).render())})),t.find("input[type=color]").each((function(e,o){r()(o).on("change",(function(){var e=t.find("#"+r()(this).data("event")).find(".note-color-btn").first(),o=this.value.toUpperCase();e.css("background-color",o).attr("aria-label",o).attr("data-value",o).attr("data-original-title",o),e.trigger("click")}))}))},click:function(e){e.stopPropagation();var o=r()("."+t).find(".note-dropdown-menu"),n=r()(e.target),a=n.data("event"),s=n.attr("data-value");if("openPalette"===a){var l=o.find("#"+s),c=r()(o.find("#"+l.data("event")).find(".note-color-row")[0]),u=c.find(".note-color-btn").last().detach(),d=l.val();u.css("background-color",d).attr("aria-label",d).attr("data-value",d).attr("data-original-title",d),c.prepend(u),l.trigger("click")}else{if(C.contains(["backColor","foreColor"],a)){var f="backColor"===a?"background-color":"color",h=n.closest(".note-color").find(".note-recent-color"),p=n.closest(".note-color").find(".note-current-color-button");h.css(f,s),p.attr("data-"+a,s)}i.context.invoke("editor."+a,s)}}})]}).render()}},{key:"addToolbarButtons",value:function(){var t=this;this.context.memo("button.style",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents(t.ui.icon(t.options.icons.magic),t.options),tooltip:t.lang.style.style,data:{toggle:"dropdown"}}),t.ui.dropdown({className:"dropdown-style",items:t.options.styleTags,title:t.lang.style.style,template:function(e){"string"==typeof e&&(e={tag:e,title:Object.prototype.hasOwnProperty.call(t.lang.style,e)?t.lang.style[e]:e});var o=e.tag,n=e.title;return"<"+o+(e.style?' style="'+e.style+'" ':"")+(e.className?' class="'+e.className+'"':"")+">"+n+""+o+">"},click:t.context.createInvokeHandler("editor.formatBlock")})]).render()}));for(var e=function(){var e=t.options.styleTags[o];t.context.memo("button.style."+e,(function(){return t.button({className:"note-btn-style-"+e,contents:''+e.toUpperCase()+"
",tooltip:t.lang.style[e],click:t.context.createInvokeHandler("editor.formatBlock")}).render()}))},o=0,n=this.options.styleTags.length;o',t.options),tooltip:t.lang.font.name,data:{toggle:"dropdown"}}),t.ui.dropdownCheck({className:"dropdown-fontname",checkClassName:t.options.icons.menuCheck,items:t.options.fontNames.filter(t.isFontInstalled.bind(t)),title:t.lang.font.name,template:function(t){return''+t+""},click:t.context.createInvokeHandlerAndUpdateState("editor.fontName")})]).render()})),this.context.memo("button.fontsize",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents('',t.options),tooltip:t.lang.font.size,data:{toggle:"dropdown"}}),t.ui.dropdownCheck({className:"dropdown-fontsize",checkClassName:t.options.icons.menuCheck,items:t.options.fontSizes,title:t.lang.font.size,click:t.context.createInvokeHandlerAndUpdateState("editor.fontSize")})]).render()})),this.context.memo("button.fontsizeunit",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents('',t.options),tooltip:t.lang.font.sizeunit,data:{toggle:"dropdown"}}),t.ui.dropdownCheck({className:"dropdown-fontsizeunit",checkClassName:t.options.icons.menuCheck,items:t.options.fontSizeUnits,title:t.lang.font.sizeunit,click:t.context.createInvokeHandlerAndUpdateState("editor.fontSizeUnit")})]).render()})),this.context.memo("button.color",(function(){return t.colorPalette("note-color-all",t.lang.color.recent,!0,!0)})),this.context.memo("button.forecolor",(function(){return t.colorPalette("note-color-fore",t.lang.color.foreground,!1,!0)})),this.context.memo("button.backcolor",(function(){return t.colorPalette("note-color-back",t.lang.color.background,!0,!1)})),this.context.memo("button.ul",(function(){return t.button({contents:t.ui.icon(t.options.icons.unorderedlist),tooltip:t.lang.lists.unordered+t.representShortcut("insertUnorderedList"),click:t.context.createInvokeHandler("editor.insertUnorderedList")}).render()})),this.context.memo("button.ol",(function(){return t.button({contents:t.ui.icon(t.options.icons.orderedlist),tooltip:t.lang.lists.ordered+t.representShortcut("insertOrderedList"),click:t.context.createInvokeHandler("editor.insertOrderedList")}).render()}));var i=this.button({contents:this.ui.icon(this.options.icons.alignLeft),tooltip:this.lang.paragraph.left+this.representShortcut("justifyLeft"),click:this.context.createInvokeHandler("editor.justifyLeft")}),a=this.button({contents:this.ui.icon(this.options.icons.alignCenter),tooltip:this.lang.paragraph.center+this.representShortcut("justifyCenter"),click:this.context.createInvokeHandler("editor.justifyCenter")}),s=this.button({contents:this.ui.icon(this.options.icons.alignRight),tooltip:this.lang.paragraph.right+this.representShortcut("justifyRight"),click:this.context.createInvokeHandler("editor.justifyRight")}),l=this.button({contents:this.ui.icon(this.options.icons.alignJustify),tooltip:this.lang.paragraph.justify+this.representShortcut("justifyFull"),click:this.context.createInvokeHandler("editor.justifyFull")}),c=this.button({contents:this.ui.icon(this.options.icons.outdent),tooltip:this.lang.paragraph.outdent+this.representShortcut("outdent"),click:this.context.createInvokeHandler("editor.outdent")}),u=this.button({contents:this.ui.icon(this.options.icons.indent),tooltip:this.lang.paragraph.indent+this.representShortcut("indent"),click:this.context.createInvokeHandler("editor.indent")});this.context.memo("button.justifyLeft",g.invoke(i,"render")),this.context.memo("button.justifyCenter",g.invoke(a,"render")),this.context.memo("button.justifyRight",g.invoke(s,"render")),this.context.memo("button.justifyFull",g.invoke(l,"render")),this.context.memo("button.outdent",g.invoke(c,"render")),this.context.memo("button.indent",g.invoke(u,"render")),this.context.memo("button.paragraph",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents(t.ui.icon(t.options.icons.alignLeft),t.options),tooltip:t.lang.paragraph.paragraph,data:{toggle:"dropdown"}}),t.ui.dropdown([t.ui.buttonGroup({className:"note-align",children:[i,a,s,l]}),t.ui.buttonGroup({className:"note-list",children:[c,u]})])]).render()})),this.context.memo("button.height",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents(t.ui.icon(t.options.icons.textHeight),t.options),tooltip:t.lang.font.height,data:{toggle:"dropdown"}}),t.ui.dropdownCheck({items:t.options.lineHeights,checkClassName:t.options.icons.menuCheck,className:"dropdown-line-height",title:t.lang.font.height,click:t.context.createInvokeHandler("editor.lineHeight")})]).render()})),this.context.memo("button.table",(function(){return t.ui.buttonGroup([t.button({className:"dropdown-toggle",contents:t.ui.dropdownButtonContents(t.ui.icon(t.options.icons.table),t.options),tooltip:t.lang.table.table,data:{toggle:"dropdown"}}),t.ui.dropdown({title:t.lang.table.table,className:"note-table",items:['",'1 x 1
'].join("")})],{callback:function(e){e.find(".note-dimension-picker-mousecatcher").css({width:t.options.insertTableMaxSize.col+"em",height:t.options.insertTableMaxSize.row+"em"}).on("mousedown",t.context.createInvokeHandler("editor.insertTable")).on("mousemove",t.tableMoveHandler.bind(t))}}).render()})),this.context.memo("button.link",(function(){return t.button({contents:t.ui.icon(t.options.icons.link),tooltip:t.lang.link.link+t.representShortcut("linkDialog.show"),click:t.context.createInvokeHandler("linkDialog.show")}).render()})),this.context.memo("button.picture",(function(){return t.button({contents:t.ui.icon(t.options.icons.picture),tooltip:t.lang.image.image,click:t.context.createInvokeHandler("imageDialog.show")}).render()})),this.context.memo("button.video",(function(){return t.button({contents:t.ui.icon(t.options.icons.video),tooltip:t.lang.video.video,click:t.context.createInvokeHandler("videoDialog.show")}).render()})),this.context.memo("button.hr",(function(){return t.button({contents:t.ui.icon(t.options.icons.minus),tooltip:t.lang.hr.insert+t.representShortcut("insertHorizontalRule"),click:t.context.createInvokeHandler("editor.insertHorizontalRule")}).render()})),this.context.memo("button.fullscreen",(function(){return t.button({className:"btn-fullscreen note-codeview-keep",contents:t.ui.icon(t.options.icons.arrowsAlt),tooltip:t.lang.options.fullscreen,click:t.context.createInvokeHandler("fullscreen.toggle")}).render()})),this.context.memo("button.codeview",(function(){return t.button({className:"btn-codeview note-codeview-keep",contents:t.ui.icon(t.options.icons.code),tooltip:t.lang.options.codeview,click:t.context.createInvokeHandler("codeview.toggle")}).render()})),this.context.memo("button.redo",(function(){return t.button({contents:t.ui.icon(t.options.icons.redo),tooltip:t.lang.history.redo+t.representShortcut("redo"),click:t.context.createInvokeHandler("editor.redo")}).render()})),this.context.memo("button.undo",(function(){return t.button({contents:t.ui.icon(t.options.icons.undo),tooltip:t.lang.history.undo+t.representShortcut("undo"),click:t.context.createInvokeHandler("editor.undo")}).render()})),this.context.memo("button.help",(function(){return t.button({contents:t.ui.icon(t.options.icons.question),tooltip:t.lang.options.help,click:t.context.createInvokeHandler("helpDialog.show")}).render()}))}},{key:"addImagePopoverButtons",value:function(){var t=this;this.context.memo("button.resizeFull",(function(){return t.button({contents:'100%',tooltip:t.lang.image.resizeFull,click:t.context.createInvokeHandler("editor.resize","1")}).render()})),this.context.memo("button.resizeHalf",(function(){return t.button({contents:'50%',tooltip:t.lang.image.resizeHalf,click:t.context.createInvokeHandler("editor.resize","0.5")}).render()})),this.context.memo("button.resizeQuarter",(function(){return t.button({contents:'25%',tooltip:t.lang.image.resizeQuarter,click:t.context.createInvokeHandler("editor.resize","0.25")}).render()})),this.context.memo("button.resizeNone",(function(){return t.button({contents:t.ui.icon(t.options.icons.rollback),tooltip:t.lang.image.resizeNone,click:t.context.createInvokeHandler("editor.resize","0")}).render()})),this.context.memo("button.floatLeft",(function(){return t.button({contents:t.ui.icon(t.options.icons.floatLeft),tooltip:t.lang.image.floatLeft,click:t.context.createInvokeHandler("editor.floatMe","left")}).render()})),this.context.memo("button.floatRight",(function(){return t.button({contents:t.ui.icon(t.options.icons.floatRight),tooltip:t.lang.image.floatRight,click:t.context.createInvokeHandler("editor.floatMe","right")}).render()})),this.context.memo("button.floatNone",(function(){return t.button({contents:t.ui.icon(t.options.icons.rollback),tooltip:t.lang.image.floatNone,click:t.context.createInvokeHandler("editor.floatMe","none")}).render()})),this.context.memo("button.removeMedia",(function(){return t.button({contents:t.ui.icon(t.options.icons.trash),tooltip:t.lang.image.remove,click:t.context.createInvokeHandler("editor.removeMedia")}).render()}))}},{key:"addLinkPopoverButtons",value:function(){var t=this;this.context.memo("button.linkDialogShow",(function(){return t.button({contents:t.ui.icon(t.options.icons.link),tooltip:t.lang.link.edit,click:t.context.createInvokeHandler("linkDialog.show")}).render()})),this.context.memo("button.unlink",(function(){return t.button({contents:t.ui.icon(t.options.icons.unlink),tooltip:t.lang.link.unlink,click:t.context.createInvokeHandler("editor.unlink")}).render()}))}},{key:"addTablePopoverButtons",value:function(){var t=this;this.context.memo("button.addRowUp",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.rowAbove),tooltip:t.lang.table.addRowAbove,click:t.context.createInvokeHandler("editor.addRow","top")}).render()})),this.context.memo("button.addRowDown",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.rowBelow),tooltip:t.lang.table.addRowBelow,click:t.context.createInvokeHandler("editor.addRow","bottom")}).render()})),this.context.memo("button.addColLeft",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.colBefore),tooltip:t.lang.table.addColLeft,click:t.context.createInvokeHandler("editor.addCol","left")}).render()})),this.context.memo("button.addColRight",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.colAfter),tooltip:t.lang.table.addColRight,click:t.context.createInvokeHandler("editor.addCol","right")}).render()})),this.context.memo("button.deleteRow",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.rowRemove),tooltip:t.lang.table.delRow,click:t.context.createInvokeHandler("editor.deleteRow")}).render()})),this.context.memo("button.deleteCol",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.colRemove),tooltip:t.lang.table.delCol,click:t.context.createInvokeHandler("editor.deleteCol")}).render()})),this.context.memo("button.deleteTable",(function(){return t.button({className:"btn-md",contents:t.ui.icon(t.options.icons.trash),tooltip:t.lang.table.delTable,click:t.context.createInvokeHandler("editor.deleteTable")}).render()}))}},{key:"build",value:function(t,e){for(var o=0,n=e.length;o3&&c3&&ul&&ac)&&(this.isFollowing=!1,this.$toolbar.css({position:"relative",top:0,width:"100%",zIndex:"auto"}),this.$editable.css({marginTop:""}))}},{key:"changeContainer",value:function(t){t?this.$toolbar.prependTo(this.$editor):this.options.toolbarContainer&&this.$toolbar.appendTo(this.options.toolbarContainer),this.options.followingToolbar&&this.followScroll()}},{key:"updateFullscreen",value:function(t){this.ui.toggleBtnActive(this.$toolbar.find(".btn-fullscreen"),t),this.changeContainer(t)}},{key:"updateCodeview",value:function(t){this.ui.toggleBtnActive(this.$toolbar.find(".btn-codeview"),t),t?this.deactivate():this.activate()}},{key:"activate",value:function(t){var e=this.$toolbar.find("button");t||(e=e.not(".note-codeview-keep")),this.ui.toggleBtn(e,!0)}},{key:"deactivate",value:function(t){var e=this.$toolbar.find("button");t||(e=e.not(".note-codeview-keep")),this.ui.toggleBtn(e,!1)}}])&&Xe(t.prototype,e),o&&Xe(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function to(t){return to="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},to(t)}function eo(t,e){for(var o=0;o','"),''),"",'','"),''),"
",this.options.disableLinkTarget?"":r()("").append(this.ui.checkbox({className:"sn-checkbox-open-in-new-window",text:this.lang.link.openInNewWindow,checked:!0}).render()).html()].join(""),o='');this.$dialog=this.ui.dialog({className:"link-dialog",title:this.lang.link.insert,fade:this.options.dialogsFade,body:e,footer:o}).render().appendTo(t)}},{key:"destroy",value:function(){this.ui.hideDialog(this.$dialog),this.$dialog.remove()}},{key:"bindEnterKey",value:function(t,e){t.on("keypress",(function(t){t.keyCode===Nt.code.ENTER&&(t.preventDefault(),e.trigger("click"))}))}},{key:"checkLinkUrl",value:function(t){return no.test(t)?"mailto://"+t:io.test(t)?"tel://"+t:ro.test(t)?t:"http://"+t}},{key:"onCheckLinkUrl",value:function(t){var e=this;t.on("blur",(function(t){t.target.value=""==t.target.value?"":e.checkLinkUrl(t.target.value)}))}},{key:"toggleLinkBtn",value:function(t,e,o){this.ui.toggleBtn(t,e.val()&&o.val())}},{key:"showLinkDialog",value:function(t){var e=this;return r().Deferred((function(o){var n=e.$dialog.find(".note-link-text"),i=e.$dialog.find(".note-link-url"),r=e.$dialog.find(".note-link-btn"),a=e.$dialog.find(".sn-checkbox-open-in-new-window input[type=checkbox]");e.ui.onDialogShown(e.$dialog,(function(){e.context.triggerEvent("dialog.shown"),!t.url&&g.isValidUrl(t.text)&&(t.url=e.checkLinkUrl(t.text)),n.on("input paste propertychange",(function(){var o=n.val(),a=document.createElement("div");a.innerText=o,o=a.innerHTML,t.text=o,e.toggleLinkBtn(r,n,i)})).val(t.text),i.on("input paste propertychange",(function(){t.text||n.val(i.val()),e.toggleLinkBtn(r,n,i)})).val(t.url),m.isSupportTouch||i.trigger("focus"),e.toggleLinkBtn(r,n,i),e.bindEnterKey(i,r),e.bindEnterKey(n,r),e.onCheckLinkUrl(i);var s=void 0!==t.isNewWindow?t.isNewWindow:e.context.options.linkTargetBlank;a.prop("checked",s),r.one("click",(function(r){r.preventDefault(),o.resolve({range:t.range,url:i.val(),text:n.val(),isNewWindow:a.is(":checked")}),e.ui.hideDialog(e.$dialog)}))})),e.ui.onDialogHidden(e.$dialog,(function(){n.off(),i.off(),r.off(),"pending"===o.state()&&o.reject()})),e.ui.showDialog(e.$dialog)})).promise()}},{key:"show",value:function(){var t=this,e=this.context.invoke("editor.getLinkInfo");this.context.invoke("editor.saveRange"),this.showLinkDialog(e).then((function(e){t.context.invoke("editor.restoreRange"),t.context.invoke("editor.createLink",e)})).fail((function(){t.context.invoke("editor.restoreRange")}))}}])&&eo(t.prototype,e),o&&eo(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function so(t){return so="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},so(t)}function lo(t,e){for(var o=0;o ')}}).render().appendTo(this.options.container);var t=this.$popover.find(".popover-content,.note-popover-content");this.context.invoke("buttons.build",t,this.options.popover.link),this.$popover.on("mousedown",(function(t){t.preventDefault()}))}},{key:"destroy",value:function(){this.$popover.remove()}},{key:"update",value:function(){if(this.context.invoke("editor.hasFocus")){var t=this.context.invoke("editor.getLastRange");if(t.isCollapsed()&&t.isOnAnchor()){var e=pt.ancestor(t.sc,pt.isAnchor),o=r()(e).attr("href");this.$popover.find("a").attr("href",o).text(o);var n=pt.posFromPlaceholder(e),i=r()(this.options.container).offset();n.top-=i.top,n.left-=i.left,this.$popover.css({display:"block",left:n.left,top:n.top})}else this.hide()}else this.hide()}},{key:"hide",value:function(){this.$popover.hide()}}])&&lo(t.prototype,e),o&&lo(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function fo(t){return fo="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},fo(t)}function ho(t,e){for(var o=0;o".concat(this.lang.image.maximumFileSize+" : "+o,"")}var n=this.options.dialogsInBody?this.$body:this.options.container,i=['','",'',t,"
",'','",'',"
"].join(""),r='');this.$dialog=this.ui.dialog({title:this.lang.image.insert,fade:this.options.dialogsFade,body:i,footer:r}).render().appendTo(n)}},{key:"destroy",value:function(){this.ui.hideDialog(this.$dialog),this.$dialog.remove()}},{key:"bindEnterKey",value:function(t,e){t.on("keypress",(function(t){t.keyCode===Nt.code.ENTER&&(t.preventDefault(),e.trigger("click"))}))}},{key:"show",value:function(){var t=this;this.context.invoke("editor.saveRange"),this.showImageDialog().then((function(e){t.ui.hideDialog(t.$dialog),t.context.invoke("editor.restoreRange"),"string"==typeof e?t.options.callbacks.onImageLinkInsert?t.context.triggerEvent("image.link.insert",e):t.context.invoke("editor.insertImage",e):t.context.invoke("editor.insertImagesOrCallback",e)})).fail((function(){t.context.invoke("editor.restoreRange")}))}},{key:"showImageDialog",value:function(){var t=this;return r().Deferred((function(e){var o=t.$dialog.find(".note-image-input"),n=t.$dialog.find(".note-image-url"),i=t.$dialog.find(".note-image-btn");t.ui.onDialogShown(t.$dialog,(function(){t.context.triggerEvent("dialog.shown"),o.replaceWith(o.clone().on("change",(function(t){e.resolve(t.target.files||t.target.value)})).val("")),n.on("input paste propertychange",(function(){t.ui.toggleBtn(i,n.val())})).val(""),m.isSupportTouch||n.trigger("focus"),i.on("click",(function(t){t.preventDefault(),e.resolve(n.val())})),t.bindEnterKey(n,i)})),t.ui.onDialogHidden(t.$dialog,(function(){o.off(),n.off(),i.off(),"pending"===e.state()&&e.reject()})),t.ui.showDialog(t.$dialog)}))}}])&&ho(t.prototype,e),o&&ho(t,o),Object.defineProperty(t,"prototype",{writable:!1}),t;var t,e,o}();function vo(t){return vo="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},vo(t)}function go(t,e){for(var o=0;o','"),''),""].join(""),o='');this.$dialog=this.ui.dialog({title:this.lang.video.insert,fade:this.options.dialogsFade,body:e,footer:o}).render().appendTo(t)}},{key:"destroy",value:function(){this.ui.hideDialog(this.$dialog),this.$dialog.remove()}},{key:"bindEnterKey",value:function(t,e){t.on("keypress",(function(t){t.keyCode===Nt.code.ENTER&&(t.preventDefault(),e.trigger("click"))}))}},{key:"createVideoNode",value:function(t){var e,o=t.match(/(?:youtu\.be\/|youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=|shorts\/|live\/))([^&\n?]+)(?:.*[?&]t=([^&\n]+))?.*/),n=t.match(/(?:\.|\/\/)drive\.google\.com\/file\/d\/(.[a-zA-Z0-9_-]*)\/view/),i=t.match(/(?:www\.|\/\/)instagram\.com\/(reel|p)\/(.[a-zA-Z0-9_-]*)/),a=t.match(/\/\/vine\.co\/v\/([a-zA-Z0-9]+)/),s=t.match(/\/\/(player\.)?vimeo\.com\/([a-z]*\/)*(\d+)[?]?.*/),l=t.match(/.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/),c=t.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/),u=t.match(/\/\/(.*)\/videos\/watch\/([^?]*)(?:\?(?:start=(\w*))?(?:&stop=(\w*))?(?:&loop=([10]))?(?:&autoplay=([10]))?(?:&muted=([10]))?)?/),d=t.match(/\/\/v\.qq\.com.*?vid=(.+)/),f=t.match(/\/\/v\.qq\.com\/x?\/?(page|cover).*?\/([^\/]+)\.html\??.*/),h=t.match(/^.+.(mp4|m4v)$/),p=t.match(/^.+.(ogg|ogv)$/),m=t.match(/^.+.(webm)$/),v=t.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/);if(o&&11===o[1].length){var g=o[1],b=0;if(void 0!==o[2]){var y=o[2].match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$/);if(y)for(var k=[3600,60,1],w=0,C=k.length;w").attr("frameborder",0).attr("src","//www.youtube.com/embed/"+g+(b>0?"?start="+b:"")).attr("width","640").attr("height","360")}else if(n&&n[0].length)e=r()("